mirror of
https://github.com/BigMarc/grammargem.git
synced 2026-10-13 17:01:55 +02:00
- Added LiveMonitor class to scan text areas in the frontmost window for grammar and spelling issues. - Introduced DetectedField struct to represent text areas with issues, including suggestions for corrections. - Implemented functionality to apply corrections and dismiss suggestions. - Added Permissions class to manage Accessibility permissions required for monitoring. - Created SnippetStore for managing text-expansion snippets, including persistence to UserDefaults. - Developed TextCapture class to read and write text from the frontmost application using Accessibility API and clipboard fallback. - Implemented TextFieldScanner to traverse the Accessibility tree and collect editable text elements. - Added TextReplacementCoordinator to orchestrate the text correction process. - Integrated UpdaterManager for handling application updates using Sparkle. - Created comprehensive unit tests for various components, including grammar corrections and entitlement checks.
84 lines
3.6 KiB
Swift
84 lines
3.6 KiB
Swift
import Foundation
|
|
import CHarper
|
|
|
|
/// Layer 1 — the real **Harper** grammar core (Apache-2.0), embedded via a Rust
|
|
/// C-FFI static lib (`harper-ffi/`, harper-core 2.5.0). Fully offline,
|
|
/// deterministic, and fast — real subject-verb agreement, verb forms,
|
|
/// punctuation, repetition, capitalization and spelling, not just a spell check.
|
|
///
|
|
/// `harper_check` returns a JSON array of spans with **char (Unicode scalar)**
|
|
/// offsets; we map those onto UTF-16 `NSRange` offsets so they compose with the
|
|
/// rest of the app (AX ranges, `NSMutableString`).
|
|
final class HarperEngine: GrammarEngine {
|
|
var ignoreList: Set<String> = []
|
|
|
|
func check(_ text: String) -> [Suggestion] {
|
|
guard !text.isEmpty else { return [] }
|
|
|
|
// Call into Rust. The returned C string is heap-allocated by Harper and
|
|
// must be freed with harper_free (it outlives the withCString buffer).
|
|
guard let raw = text.withCString({ harper_check($0) }) else { return [] }
|
|
defer { harper_free(raw) }
|
|
|
|
let json = String(cString: raw)
|
|
guard let data = json.data(using: .utf8),
|
|
let lints = try? JSONDecoder().decode([HarperLint].self, from: data)
|
|
else { return [] }
|
|
|
|
// Harper can emit several lints over the SAME span (e.g. two phrasings of
|
|
// "checkin" -> "check in"). Keep the first (highest-priority) per span so
|
|
// the right-to-left bulk apply in `correct()` never double-writes a range.
|
|
var kept: [Suggestion] = []
|
|
for lint in lints {
|
|
guard lint.len > 0 || !lint.replacement.isEmpty else { continue }
|
|
guard let range = Self.utf16Range(scalarStart: lint.start, scalarLen: lint.len, in: text)
|
|
else { continue }
|
|
let original = (text as NSString).substring(with: range)
|
|
if !original.isEmpty, ignoreList.contains(original.lowercased()) { continue }
|
|
if kept.contains(where: { NSIntersectionRange($0.range, range).length > 0 }) { continue }
|
|
kept.append(Suggestion(
|
|
location: range.location, length: range.length,
|
|
original: original, replacement: lint.replacement,
|
|
kind: GrammarKind(harperKind: lint.kind), message: lint.message))
|
|
}
|
|
return kept.sorted { $0.location < $1.location }
|
|
}
|
|
|
|
// MARK: - FFI decoding
|
|
|
|
private struct HarperLint: Decodable {
|
|
let start: Int
|
|
let len: Int
|
|
let replacement: String
|
|
let message: String
|
|
let kind: String
|
|
}
|
|
|
|
/// Map a Harper char-span (Unicode scalar offsets, end-exclusive) onto a
|
|
/// UTF-16 `NSRange`. Scalar boundaries are always UTF-16 boundaries, so the
|
|
/// conversion is exact.
|
|
static func utf16Range(scalarStart: Int, scalarLen: Int, in text: String) -> NSRange? {
|
|
let scalars = text.unicodeScalars
|
|
guard let s = scalars.index(scalars.startIndex, offsetBy: scalarStart, limitedBy: scalars.endIndex),
|
|
let e = scalars.index(s, offsetBy: scalarLen, limitedBy: scalars.endIndex)
|
|
else { return nil }
|
|
let location = text.utf16.distance(from: text.utf16.startIndex, to: s)
|
|
let length = text.utf16.distance(from: s, to: e)
|
|
return NSRange(location: location, length: length)
|
|
}
|
|
}
|
|
|
|
extension GrammarKind {
|
|
/// Map Harper's `kind` string (see harper.h) onto our category enum.
|
|
init(harperKind kind: String) {
|
|
switch kind {
|
|
case "spelling": self = .spelling
|
|
case "punctuation": self = .punctuation
|
|
case "repetition": self = .repetition
|
|
case "capitalization": self = .capitalization
|
|
case "phrasing": self = .phrasing
|
|
default: self = .grammar
|
|
}
|
|
}
|
|
}
|