From dd7590be6e83eaffc2f21539186c82735dbbce06 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sat, 24 Feb 2024 15:16:09 -0600 Subject: [PATCH 1/4] Misc UndoManager Fixes --- .../TextView/TextView+ReplaceCharacters.swift | 10 ---------- Sources/CodeEditTextView/Utils/CEUndoManager.swift | 7 +++++++ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift b/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift index 333f41a2..0aa7f6a9 100644 --- a/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift +++ b/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift @@ -21,12 +21,6 @@ extension TextView { layoutManager.beginTransaction() textStorage.beginEditing() - var shouldEndGrouping = false - if !(_undoManager?.isGrouping ?? false) { - _undoManager?.beginGrouping() - shouldEndGrouping = 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) && @@ -46,10 +40,6 @@ extension TextView { delegate?.textView(self, didReplaceContentsIn: range, with: string) } - if shouldEndGrouping { - _undoManager?.endGrouping() - } - layoutManager.endTransaction() textStorage.endEditing() selectionManager.notifyAfterEdit() diff --git a/Sources/CodeEditTextView/Utils/CEUndoManager.swift b/Sources/CodeEditTextView/Utils/CEUndoManager.swift index 86eca088..049084a3 100644 --- a/Sources/CodeEditTextView/Utils/CEUndoManager.swift +++ b/Sources/CodeEditTextView/Utils/CEUndoManager.swift @@ -22,6 +22,8 @@ public class CEUndoManager { public class DelegatedUndoManager: UndoManager { weak var parent: CEUndoManager? + public override var isUndoing: Bool { parent?.isUndoing ?? false } + public override var isRedoing: Bool { parent?.isRedoing ?? false } public override var canUndo: Bool { parent?.canUndo ?? false } public override var canRedo: Bool { parent?.canRedo ?? false } @@ -97,9 +99,11 @@ public class CEUndoManager { } isUndoing = true NotificationCenter.default.post(name: .NSUndoManagerWillUndoChange, object: self.manager) + textView.textStorage.beginEditing() for mutation in item.mutations.reversed() { textView.replaceCharacters(in: mutation.inverse.range, with: mutation.inverse.string) } + textView.textStorage.endEditing() NotificationCenter.default.post(name: .NSUndoManagerDidUndoChange, object: self.manager) redoStack.append(item) isUndoing = false @@ -112,9 +116,11 @@ public class CEUndoManager { } isRedoing = true NotificationCenter.default.post(name: .NSUndoManagerWillRedoChange, object: self.manager) + textView.textStorage.beginEditing() for mutation in item.mutations { textView.replaceCharacters(in: mutation.mutation.range, with: mutation.mutation.string) } + textView.textStorage.endEditing() NotificationCenter.default.post(name: .NSUndoManagerDidRedoChange, object: self.manager) undoStack.append(item) isRedoing = false @@ -188,6 +194,7 @@ public class CEUndoManager { /// - lastMutation: The last mutation applied to the document. /// - Returns: Whether or not the given mutations can be grouped. private func shouldContinueGroup(_ mutation: Mutation, lastMutation: Mutation) -> Bool { + return false // If last mutation was delete & new is insert or vice versa, split group if (mutation.mutation.range.length > 0 && lastMutation.mutation.range.length == 0) || (mutation.mutation.range.length == 0 && lastMutation.mutation.range.length > 0) { From 58a095ca36ba5e0798cad362eb6db122a1818c1b Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sat, 24 Feb 2024 15:20:42 -0600 Subject: [PATCH 2/4] Break Up TextLayoutManager --- .../TextLayoutManager+Invalidation.swift | 34 +++++++++++ .../TextLayoutManager+Transaction.swift | 37 ++++++++++++ .../TextLayoutManager/TextLayoutManager.swift | 59 +------------------ 3 files changed, 74 insertions(+), 56 deletions(-) create mode 100644 Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Invalidation.swift create mode 100644 Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Transaction.swift diff --git a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Invalidation.swift b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Invalidation.swift new file mode 100644 index 00000000..6ddb9a30 --- /dev/null +++ b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Invalidation.swift @@ -0,0 +1,34 @@ +// +// TextLayoutManager+Invalidation.swift +// CodeEditTextView +// +// Created by Khan Winter on 2/24/24. +// + +import Foundation + +extension TextLayoutManager { + /// Invalidates layout for the given rect. + /// - Parameter rect: The rect to invalidate. + public func invalidateLayoutForRect(_ rect: NSRect) { + for linePosition in lineStorage.linesStartingAt(rect.minY, until: rect.maxY) { + linePosition.data.setNeedsLayout() + } + layoutLines() + } + + /// Invalidates layout for the given range of text. + /// - Parameter range: The range of text to invalidate. + public func invalidateLayoutForRange(_ range: NSRange) { + for linePosition in lineStorage.linesInRange(range) { + linePosition.data.setNeedsLayout() + } + + layoutLines() + } + + public func setNeedsLayout() { + needsLayout = true + visibleLineIds.removeAll(keepingCapacity: true) + } +} diff --git a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Transaction.swift b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Transaction.swift new file mode 100644 index 00000000..b55f3032 --- /dev/null +++ b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Transaction.swift @@ -0,0 +1,37 @@ +// +// TextLayoutManager+Transaction.swift +// CodeEditTextView +// +// Created by Khan Winter on 2/24/24. +// + +import Foundation + +extension TextLayoutManager { + /// Begins a transaction, preventing the layout manager from performing layout until the `endTransaction` is called. + /// Useful for grouping attribute modifications into one layout pass rather than laying out every update. + /// + /// You can nest transaction start/end calls, the layout manager will not cause layout until the last transaction + /// group is ended. + /// + /// Ensure there is a balanced number of begin/end calls. If there is a missing endTranscaction call, the layout + /// manager will never lay out text. If there is a end call without matching a start call an assertionFailure + /// will occur. + public func beginTransaction() { + transactionCounter += 1 + } + + /// Ends a transaction. When called, the layout manager will layout any necessary lines. + public func endTransaction(forceLayout: Bool = false) { + transactionCounter -= 1 + if transactionCounter == 0 { + if forceLayout { + setNeedsLayout() + } + layoutLines() + } else if transactionCounter < 0 { + // swiftlint:disable:next line_length + assertionFailure("TextLayoutManager.endTransaction called without a matching TextLayoutManager.beginTransaction call") + } + } +} diff --git a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift index 11ce033d..6de10a04 100644 --- a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift +++ b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift @@ -71,11 +71,11 @@ public class TextLayoutManager: NSObject { var lineStorage: TextLineStorage = TextLineStorage() var markedTextManager: MarkedTextManager = MarkedTextManager() private let viewReuseQueue: ViewReuseQueue = ViewReuseQueue() - private var visibleLineIds: Set = [] + package var visibleLineIds: Set = [] /// Used to force a complete re-layout using `setNeedsLayout` - private var needsLayout: Bool = false + package var needsLayout: Bool = false - private var transactionCounter: Int = 0 + package var transactionCounter: Int = 0 public var isInTransaction: Bool { transactionCounter > 0 } @@ -186,59 +186,6 @@ public class TextLayoutManager: NSObject { /// ``TextLayoutManager/estimateLineHeight()`` is called. private var _estimateLineHeight: CGFloat? - // MARK: - Invalidation - - /// Invalidates layout for the given rect. - /// - Parameter rect: The rect to invalidate. - public func invalidateLayoutForRect(_ rect: NSRect) { - for linePosition in lineStorage.linesStartingAt(rect.minY, until: rect.maxY) { - linePosition.data.setNeedsLayout() - } - layoutLines() - } - - /// Invalidates layout for the given range of text. - /// - Parameter range: The range of text to invalidate. - public func invalidateLayoutForRange(_ range: NSRange) { - for linePosition in lineStorage.linesInRange(range) { - linePosition.data.setNeedsLayout() - } - - layoutLines() - } - - public func setNeedsLayout() { - needsLayout = true - visibleLineIds.removeAll(keepingCapacity: true) - } - - /// Begins a transaction, preventing the layout manager from performing layout until the `endTransaction` is called. - /// Useful for grouping attribute modifications into one layout pass rather than laying out every update. - /// - /// You can nest transaction start/end calls, the layout manager will not cause layout until the last transaction - /// group is ended. - /// - /// Ensure there is a balanced number of begin/end calls. If there is a missing endTranscaction call, the layout - /// manager will never lay out text. If there is a end call without matching a start call an assertionFailure - /// will occur. - public func beginTransaction() { - transactionCounter += 1 - } - - /// Ends a transaction. When called, the layout manager will layout any necessary lines. - public func endTransaction(forceLayout: Bool = false) { - transactionCounter -= 1 - if transactionCounter == 0 { - if forceLayout { - setNeedsLayout() - } - layoutLines() - } else if transactionCounter < 0 { - // swiftlint:disable:next line_length - assertionFailure("TextLayoutManager.endTransaction called without a matching TextLayoutManager.beginTransaction call") - } - } - // MARK: - Layout /// Lays out all visible lines From efe5346d2c798c62014076c945540423ff6dacc1 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 29 Feb 2024 14:03:00 -0600 Subject: [PATCH 3/4] Remove False Return, Strengthen Line Ending Check --- Sources/CodeEditTextView/Utils/CEUndoManager.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/CodeEditTextView/Utils/CEUndoManager.swift b/Sources/CodeEditTextView/Utils/CEUndoManager.swift index 049084a3..e9ce1223 100644 --- a/Sources/CodeEditTextView/Utils/CEUndoManager.swift +++ b/Sources/CodeEditTextView/Utils/CEUndoManager.swift @@ -194,7 +194,6 @@ public class CEUndoManager { /// - lastMutation: The last mutation applied to the document. /// - Returns: Whether or not the given mutations can be grouped. private func shouldContinueGroup(_ mutation: Mutation, lastMutation: Mutation) -> Bool { - return false // If last mutation was delete & new is insert or vice versa, split group if (mutation.mutation.range.length > 0 && lastMutation.mutation.range.length == 0) || (mutation.mutation.range.length == 0 && lastMutation.mutation.range.length > 0) { @@ -205,7 +204,7 @@ public class CEUndoManager { // Deleting return ( lastMutation.mutation.range.location == mutation.mutation.range.max - && mutation.inverse.string != "\n" + && LineEnding(line: lastMutation.inverse.string) == nil ) } else { // Inserting @@ -214,14 +213,14 @@ public class CEUndoManager { // If the last mutation was not whitespace, and the new one is, break the group. if lastMutation.mutation.string.count < 1024 && mutation.mutation.string.count < 1024 - && !lastMutation.mutation.string.trimmingCharacters(in: .whitespaces).isEmpty + && !lastMutation.mutation.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && mutation.mutation.string.trimmingCharacters(in: .whitespaces).isEmpty { return false } return ( lastMutation.mutation.range.max + 1 == mutation.mutation.range.location - && mutation.mutation.string != "\n" + && LineEnding(line: mutation.mutation.string) == nil ) } } From 28aa52b6725c43735d6d907c412c959c3fe46456 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 1 Mar 2024 12:30:20 -0600 Subject: [PATCH 4/4] Update TextLayoutManager+Transaction.swift --- .../TextLayoutManager/TextLayoutManager+Transaction.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Transaction.swift b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Transaction.swift index b55f3032..c160bfd5 100644 --- a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Transaction.swift +++ b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Transaction.swift @@ -30,8 +30,9 @@ extension TextLayoutManager { } layoutLines() } else if transactionCounter < 0 { - // swiftlint:disable:next line_length - assertionFailure("TextLayoutManager.endTransaction called without a matching TextLayoutManager.beginTransaction call") + assertionFailure( + "TextLayoutManager.endTransaction called without a matching TextLayoutManager.beginTransaction call" + ) } } }