Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -197,7 +197,7 @@ public class TextSelectionManager: NSObject {
}
}

if didUpdate {
if didUpdate || force {
delegate?.setNeedsDisplay()
cursorTimer.resetTimer()
resetSystemCursorTimers()
Expand Down
54 changes: 46 additions & 8 deletions Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -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)
}
}