@@ -29,6 +29,25 @@ public final class MessageAutocompleteController: MessageTextViewListener {
2929 public let range : NSRange
3030 }
3131 public private( set) var selection : Selection ?
32+
33+ /// Adds an additional space after the autocompleted text when true. Default value is `TRUE`
34+ open var appendSpaceOnCompletion = true
35+
36+ /// The default text attributes
37+ open var defaultTextAttributes : [ NSAttributedStringKey : Any ] = [ . font: UIFont . preferredFont ( forTextStyle: . body) , . foregroundColor: UIColor . black]
38+
39+ /// The text attributes applied to highlighted substrings for each prefix
40+ open var autocompleteTextAttributes : [ String : [ NSAttributedStringKey : Any ] ] = [ : ]
41+
42+ /// A key used for referencing which substrings were autocompletes
43+ private let NSAttributedAutocompleteKey = NSAttributedStringKey . init ( " com.messageviewcontroller.autocompletekey " )
44+
45+ /// A reference to `defaultTextAttributes` that adds the NSAttributedAutocompleteKey
46+ private var typingTextAttributes : [ NSAttributedStringKey : Any ] {
47+ var attributes = defaultTextAttributes
48+ attributes [ NSAttributedAutocompleteKey] = false
49+ return attributes
50+ }
3251
3352 internal var registeredPrefixes = Set < String > ( )
3453 internal let border = CALayer ( )
@@ -80,12 +99,18 @@ public final class MessageAutocompleteController: MessageTextViewListener {
8099 )
81100
82101 guard let range = Range ( insertionRange, in: text) else { return }
83-
84- textView. text = text. replacingCharacters ( in: range, with: autocomplete)
102+
103+ // Create an NSRange to use with attributedText replacement
104+ let nsrange = NSRange ( range, in: textView. text)
105+ insertAutocomplete ( autocomplete, at: selection, for: nsrange, keepPrefix: keepPrefix)
106+
107+ let selectedLocation = insertionRange. location + autocomplete. utf16. count + ( appendSpaceOnCompletion ? 1 : 0 )
85108 textView. selectedRange = NSRange (
86- location: insertionRange . location + autocomplete . utf16 . count ,
109+ location: selectedLocation ,
87110 length: 0
88111 )
112+
113+ preserveTypingAttributes ( for: textView)
89114 }
90115
91116 internal func cancel( ) {
@@ -139,6 +164,26 @@ public final class MessageAutocompleteController: MessageTextViewListener {
139164 }
140165
141166 // MARK: Private API
167+
168+ private func insertAutocomplete( _ autocomplete: String , at selection: Selection , for range: NSRange , keepPrefix: Bool ) {
169+
170+ // Apply the autocomplete attributes
171+ var attrs = autocompleteTextAttributes [ selection. prefix] ?? defaultTextAttributes
172+ attrs [ NSAttributedAutocompleteKey] = true
173+ let newString = ( keepPrefix ? selection. prefix : " " ) + autocomplete
174+ let newAttributedString = NSAttributedString ( string: newString, attributes: attrs)
175+
176+ // Modify the NSRange to include the prefix length
177+ let rangeModifier = keepPrefix ? selection. prefix. count : 0
178+ let highlightedRange = NSRange ( location: range. location - rangeModifier, length: range. length + rangeModifier)
179+
180+ // Replace the attributedText with a modified version including the autocompete
181+ let newAttributedText = textView. attributedText. replacingCharacters ( in: highlightedRange, with: newAttributedString)
182+ if appendSpaceOnCompletion {
183+ newAttributedText. append ( NSAttributedString ( string: " " , attributes: typingTextAttributes) )
184+ }
185+ textView. attributedText = newAttributedText
186+ }
142187
143188 internal func check( ) {
144189 guard let result = textView. find ( prefixes: registeredPrefixes) else {
@@ -156,13 +201,54 @@ public final class MessageAutocompleteController: MessageTextViewListener {
156201 else { return }
157202 keyboardHeight = keyboardFrame. height
158203 }
204+
205+ /// Ensures new text typed is not styled
206+ ///
207+ /// - Parameter textView: The `UITextView` to apply `typingTextAttributes` to
208+ internal func preserveTypingAttributes( for textView: UITextView ) {
209+ var typingAttributes = [ String: Any] ( )
210+ typingTextAttributes. forEach { typingAttributes [ $0. key. rawValue] = $0. value }
211+ textView. typingAttributes = typingAttributes
212+ }
159213
160214 // MARK: MessageTextViewListener
161215
162216 public func didChangeSelection( textView: MessageTextView ) {
163217 check ( )
164218 }
165219
166- public func didChange( textView: MessageTextView ) { }
220+ public func didChange( textView: MessageTextView ) {
221+ preserveTypingAttributes ( for: textView)
222+ }
223+
224+ public func willChangeRange( textView: MessageTextView , to range: NSRange ) {
225+
226+ // range.length > 0: Backspace/removing text
227+ // range.lowerBound < textView.selectedRange.lowerBound: Ignore trying to delete
228+ // the substring if the user is already doing so
229+ if range. length > 0 , range. lowerBound < textView. selectedRange. lowerBound {
230+
231+ // Backspace/removing text
232+ let attribute = textView. attributedText
233+ . attributes ( at: range. lowerBound, longestEffectiveRange: nil , in: range)
234+ . filter { return $0. key == NSAttributedAutocompleteKey }
235+
236+ if ( attribute [ NSAttributedAutocompleteKey] as? Bool ?? false ) == true {
237+
238+ // Remove the autocompleted substring
239+ let lowerRange = NSRange ( location: 0 , length: range. location + 1 )
240+ textView. attributedText. enumerateAttribute ( NSAttributedAutocompleteKey, in: lowerRange, options: . reverse, using: { ( _, range, stop) in
241+
242+ // Only delete the first found range
243+ defer { stop. pointee = true }
244+
245+ let emptyString = NSAttributedString ( string: " " , attributes: typingTextAttributes)
246+ textView. attributedText = textView. attributedText. replacingCharacters ( in: range, with: emptyString)
247+ textView. selectedRange = NSRange ( location: range. location, length: 0 )
248+ self . preserveTypingAttributes ( for: textView)
249+ } )
250+ }
251+ }
252+ }
167253
168254}
0 commit comments