#if canImport(UIKit) import UIKit // MARK: - Applying snippets to a UITextView extension EditorSnippet { /// Applies this snippet to `textView` at its current insertion point, /// updating the text and cursor, then notifying the text view's delegate /// (UITextView does not report programmatic edits on its own). @MainActor public func apply(to textView: UITextView) { let text = textView.text ?? "" let result = apply( to: text, cursor: Self.characterOffset(of: textView.selectedRange, in: text)) // Replacing `text` wholesale resets the scroll to the top, so preserve // and restore the content offset around the edit. let savedOffset = textView.contentOffset textView.text = result.text textView.layoutIfNeeded() textView.selectedRange = Self.range(forCharacterOffset: result.cursor, in: result.text) textView.setContentOffset(savedOffset, animated: false) textView.delegate?.textViewDidChange?(textView) } /// The character (grapheme) offset of a UTF-16 `NSRange`'s start. private static func characterOffset(of nsRange: NSRange, in text: String) -> Int { guard let range = Range(nsRange, in: text) else { return text.count } return text.distance(from: text.startIndex, to: range.lowerBound) } /// A zero-length UTF-16 `NSRange` at the given character offset. private static func range(forCharacterOffset offset: Int, in text: String) -> NSRange { let clamped = min(max(offset, 0), text.count) let index = text.index(text.startIndex, offsetBy: clamped) return NSRange(index.. UIBarButtonItem in let image = UIImage( systemName: snippet.systemImage, withConfiguration: Self.symbolConfiguration) let action = UIAction(image: image) { [weak self] _ in guard let textView = self?.textView else { return } snippet.apply(to: textView) } let item = UIBarButtonItem(primaryAction: action) item.accessibilityLabel = snippet.label return item } // Center the group of buttons with flexible space on both sides. setItems([.flexibleSpace()] + buttons + [.flexibleSpace()], animated: false) } } extension UITextView { /// Installs a ``NorgKeyboardAccessory`` as this text view's input accessory. public func installNorgKeyboardAccessory() { inputAccessoryView = NorgKeyboardAccessory(textView: self) } } #endif