krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/OrgEditorAppKit/Accessibility.swift

3a3dedba062790e1580c66d38606eda4b02cb501
orgstar/Sources/OrgEditorAppKit/Accessibility.swift history · blame · raw

189 lines · 7656 bytes

  1#if os(macOS)
  2import AppKit
  3
  4/// The text as shown, for VoiceOver: without hidden markup (link brackets and targets, hidden
  5/// emphasis markers and stars), with pretty entities and shrunk table cells as they're drawn.
  6/// Folded text is still read, so the whole file can be. `model` maps each UTF-16 unit of
  7/// `string` to the buffer.
  8struct AccessibleText {
  9    let string: String
 10    /// Buffer offset of each unit of `string`, then the buffer's length.
 11    let model: [Int]
 12
 13    init(_ storage: NSAttributedString) {
 14        let ns = storage.string as NSString
 15        let whole = NSRange(location: 0, length: ns.length)
 16        // What isn't read: markup drawn at no size. Entities read as drawn.
 17        var skipped: [Range<Int>] = []
 18        storage.enumerateAttribute(.font, in: whole) { value, run, _ in
 19            if let font = value as? NSFont, font.pointSize < 0.1 { skipped.append(run.location..<NSMaxRange(run)) }
 20        }
 21        var displays: [(at: Int, text: String)] = []
 22        storage.enumerateAttribute(.orgEntityDisplay, in: whole) { value, run, _ in
 23            if let text = value as? String { displays.append((run.location, text)) }
 24        }
 25        skipped.sort { $0.lowerBound < $1.lowerBound }
 26        var units = [unichar](repeating: 0, count: ns.length)
 27        units.withUnsafeMutableBufferPointer { ns.getCharacters($0.baseAddress!, range: whole) }
 28        var outUnits: [unichar] = []
 29        var model: [Int] = []
 30        outUnits.reserveCapacity(ns.length)
 31        model.reserveCapacity(ns.length + 1)
 32        var next = 0
 33        var display = 0
 34        func copy(_ from: Int, _ to: Int) {
 35            var p = from
 36            while p < to {
 37                while display < displays.count, displays[display].at < p { display += 1 }
 38                let stop = display < displays.count ? min(to, displays[display].at) : to
 39                outUnits.append(contentsOf: units[p..<stop])
 40                model.append(contentsOf: p..<stop)
 41                p = stop
 42                if p < to {
 43                    for unit in displays[display].text.utf16 {
 44                        outUnits.append(unit)
 45                        model.append(p)
 46                    }
 47                    p += 1
 48                    display += 1
 49                }
 50            }
 51        }
 52        for skip in skipped where skip.upperBound > next {
 53            if skip.lowerBound > next { copy(next, skip.lowerBound) }
 54            next = max(next, skip.upperBound)
 55        }
 56        copy(next, ns.length)
 57        model.append(ns.length)
 58        string = String(utf16CodeUnits: outUnits, count: outUnits.count)
 59        self.model = model
 60    }
 61
 62    var length: Int { model.count - 1 }
 63
 64    /// The shown offset at or after buffer offset `offset`.
 65    func visible(_ offset: Int) -> Int {
 66        var low = 0
 67        var high = length
 68        while low < high {
 69            let mid = (low + high) / 2
 70            if model[mid] < offset { low = mid + 1 } else { high = mid }
 71        }
 72        return low
 73    }
 74
 75    func visible(_ range: NSRange) -> NSRange {
 76        let start = visible(range.location)
 77        return NSRange(location: start, length: max(0, visible(NSMaxRange(range)) - start))
 78    }
 79
 80    /// The buffer range a shown range covers.
 81    func buffer(_ range: NSRange) -> NSRange {
 82        let start = model[min(max(0, range.location), length)]
 83        let endIndex = min(max(0, NSMaxRange(range)), length)
 84        let end = endIndex > range.location && endIndex > 0 ? model[endIndex - 1] + 1 : model[endIndex]
 85        return NSRange(location: start, length: max(0, end - start))
 86    }
 87
 88    /// Shown lines: line `n` is the text between the n-th and (n+1)-th newline.
 89    func line(for index: Int) -> Int {
 90        let ns = string as NSString
 91        var line = 0
 92        var p = 0
 93        while p < min(index, ns.length) {
 94            let next = ns.range(of: "\n", range: NSRange(location: p, length: min(index, ns.length) - p))
 95            guard next.location != NSNotFound else { break }
 96            line += 1
 97            p = next.location + 1
 98        }
 99        return line
100    }
101
102    func range(forLine line: Int) -> NSRange {
103        let ns = string as NSString
104        var start = 0
105        for _ in 0..<line {
106            let next = ns.range(of: "\n", range: NSRange(location: start, length: ns.length - start))
107            guard next.location != NSNotFound else { return NSRange(location: ns.length, length: 0) }
108            start = next.location + 1
109        }
110        return ns.lineRange(for: NSRange(location: start, length: 0))
111    }
112}
113
114extension OrgTextView {
115    private var shown: AccessibleText? { accessibleText?() }
116
117    override func accessibilityValue() -> String? {
118        shown?.string ?? super.accessibilityValue()
119    }
120
121    override func accessibilityNumberOfCharacters() -> Int {
122        shown?.length ?? super.accessibilityNumberOfCharacters()
123    }
124
125    override func accessibilitySelectedTextRange() -> NSRange {
126        guard let shown else { return super.accessibilitySelectedTextRange() }
127        return shown.visible(selectedRange())
128    }
129
130    override func setAccessibilitySelectedTextRange(_ range: NSRange) {
131        guard let shown else { return super.setAccessibilitySelectedTextRange(range) }
132        setSelectedRange(shown.buffer(range))
133    }
134
135    override func accessibilitySelectedText() -> String? {
136        guard let shown else { return super.accessibilitySelectedText() }
137        return (shown.string as NSString).substring(with: shown.visible(selectedRange()))
138    }
139
140    override func accessibilityVisibleCharacterRange() -> NSRange {
141        guard let shown else { return super.accessibilityVisibleCharacterRange() }
142        return shown.visible(super.accessibilityVisibleCharacterRange())
143    }
144
145    override func accessibilityString(for range: NSRange) -> String? {
146        guard let shown else { return super.accessibilityString(for: range) }
147        let ns = shown.string as NSString
148        guard range.location >= 0, NSMaxRange(range) <= ns.length else { return nil }
149        return ns.substring(with: range)
150    }
151
152    override func accessibilityAttributedString(for range: NSRange) -> NSAttributedString? {
153        accessibilityString(for: range).map { NSAttributedString(string: $0) }
154    }
155
156    override func accessibilityLine(for index: Int) -> Int {
157        shown?.line(for: index) ?? super.accessibilityLine(for: index)
158    }
159
160    override func accessibilityInsertionPointLineNumber() -> Int {
161        guard let shown else { return super.accessibilityInsertionPointLineNumber() }
162        return shown.line(for: shown.visible(selectedRange().location))
163    }
164
165    override func accessibilityRange(forLine line: Int) -> NSRange {
166        shown?.range(forLine: line) ?? super.accessibilityRange(forLine: line)
167    }
168
169    override func accessibilityRange(for index: Int) -> NSRange {
170        guard let shown else { return super.accessibilityRange(for: index) }
171        return (shown.string as NSString).rangeOfComposedCharacterSequence(at: min(index, max(0, shown.length - 1)))
172    }
173
174    override func accessibilityStyleRange(for index: Int) -> NSRange {
175        accessibilityRange(for: index)
176    }
177
178    override func accessibilityFrame(for range: NSRange) -> NSRect {
179        guard let shown else { return super.accessibilityFrame(for: range) }
180        return super.accessibilityFrame(for: shown.buffer(range))
181    }
182
183    override func accessibilityRange(for point: NSPoint) -> NSRange {
184        guard let shown else { return super.accessibilityRange(for: point) }
185        let range = super.accessibilityRange(for: point)
186        return shown.visible(range)
187    }
188}
189#endif