diff --git a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+Update.swift b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+Update.swift index 8ea0c7490..0c319681d 100644 --- a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+Update.swift +++ b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+Update.swift @@ -39,8 +39,8 @@ extension TextSelectionManager { } } - func notifyAfterEdit() { - updateSelectionViews() + public func notifyAfterEdit(force: Bool = false) { + updateSelectionViews(force: force) NotificationCenter.default.post(Notification(name: Self.selectionChangedNotification, object: self)) } } diff --git a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift index a56c73a68..9df5922bd 100644 --- a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift +++ b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift @@ -138,7 +138,7 @@ public class TextSelectionManager: NSObject { /// Update all selection cursors. Placing them in the correct position for each text selection and reseting the /// blink timer. - func updateSelectionViews() { + func updateSelectionViews(force: Bool = false) { guard textView?.isFirstResponder ?? false else { return } var didUpdate: Bool = false @@ -197,7 +197,7 @@ public class TextSelectionManager: NSObject { } } - if didUpdate { + if didUpdate || force { delegate?.setNeedsDisplay() cursorTimer.resetTimer() resetSystemCursorTimers() diff --git a/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift b/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift index 951e9977e..6acd0d040 100644 --- a/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift +++ b/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift @@ -13,15 +13,23 @@ extension TextView { /// - Parameters: /// - ranges: The ranges to replace /// - string: The string to insert in the ranges. - public func replaceCharacters(in ranges: [NSRange], with string: String) { + /// - skipUpdateSelection: Skips the selection update step + public func replaceCharacters( + in ranges: [NSRange], + with string: String, + skipUpdateSelection: Bool = false + ) { guard isEditable else { return } NotificationCenter.default.post(name: Self.textWillChangeNotification, object: self) textStorage.beginEditing() + func valid(range: NSRange, string: String) -> Bool { + (!range.isEmpty || !string.isEmpty) && + (delegate?.textView(self, shouldReplaceContentsIn: range, with: string) ?? true) + } + // Can't insert an empty string into an empty range. One must be not empty - for range in ranges.sorted(by: { $0.location > $1.location }) where - (!range.isEmpty || !string.isEmpty) && - (delegate?.textView(self, shouldReplaceContentsIn: range, with: string) ?? true) { + for range in ranges.sorted(by: { $0.location > $1.location }) where valid(range: range, string: string) { delegate?.textView(self, willReplaceContentsIn: range, with: string) _undoManager?.registerMutation( @@ -31,13 +39,17 @@ extension TextView { in: range, with: NSAttributedString(string: string, attributes: typingAttributes) ) - selectionManager.didReplaceCharacters(in: range, replacementLength: (string as NSString).length) + if !skipUpdateSelection { + selectionManager.didReplaceCharacters(in: range, replacementLength: (string as NSString).length) + } delegate?.textView(self, didReplaceContentsIn: range, with: string) } textStorage.endEditing() - selectionManager.notifyAfterEdit() + if !skipUpdateSelection { + selectionManager.notifyAfterEdit() + } NotificationCenter.default.post(name: Self.textDidChangeNotification, object: self) // `scrollSelectionToVisible` is a little expensive to call every time. Instead we just check if the first @@ -51,7 +63,33 @@ extension TextView { /// - Parameters: /// - range: The range to replace. /// - string: The string to insert in the range. - public func replaceCharacters(in range: NSRange, with string: String) { - replaceCharacters(in: [range], with: string) + /// - skipUpdateSelection: Skips the selection update step + public func replaceCharacters( + in range: NSRange, + with string: String, + skipUpdateSelection: Bool = false + ) { + replaceCharacters(in: [range], with: string, skipUpdateSelection: skipUpdateSelection) + } + + /// Iterates over all text selections in the `TextView` and applies the provided callback. + /// + /// This method is typically used when you need to perform an operation on each text selection in the editor, + /// such as adjusting indentation, or other selection-based operations. The callback + /// is executed for each selection, and you can modify the selection or perform related tasks. + /// + /// - Parameters: + /// - callback: A closure that will be executed for each selection in the `TextView`. It takes two parameters: + /// a `TextView` instance, allowing access to the view's properties and methods and a + /// `TextSelectionManager.TextSelection` representing the current selection to operate on. + /// + /// - Note: The selections are iterated in reverse order, so modifications to earlier selections won't affect later + /// ones. The method automatically calls `notifyAfterEdit()` on the `selectionManager` after all + /// selections are processed. + public func editSelections(callback: (TextView, TextSelectionManager.TextSelection) -> Void) { + for textSelection in selectionManager.textSelections.reversed() { + callback(self, textSelection) + } + selectionManager.notifyAfterEdit(force: true) } }