Skip to content

Commit c127b47

Browse files
NSBeep On Empty Undo, Remove Unnecessary Redirection (#102)
### Description - When the undo or redo stack is empty, and the user tries to undo or redo, plays the beep sound. - Removes the (now) unnecessary `DelegatedUndoManager` type that sent messages to the `CEUndoManager` type. This was a holdover from when we did not have a custom text view. - Made `CEUndoManager` a subclass of `UndoManager` - Added more overrides to more correctly conform to the `UndoManager` class. ### Related Issues * closes #89 ### Checklist - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots
1 parent 2508635 commit c127b47

File tree

3 files changed

+59
-67
lines changed

3 files changed

+59
-67
lines changed

Sources/CodeEditTextView/TextView/TextView+UndoRedo.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ extension TextView {
1414
}
1515

1616
override public var undoManager: UndoManager? {
17-
_undoManager?.manager
17+
_undoManager
1818
}
1919

2020
@objc func undo(_ sender: AnyObject?) {

Sources/CodeEditTextView/Utils/CEUndoManager.swift

Lines changed: 44 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -15,45 +15,7 @@ import TextStory
1515
/// - Grouping pasted text
1616
///
1717
/// If needed, the automatic undo grouping can be overridden using the `beginGrouping()` and `endGrouping()` methods.
18-
public class CEUndoManager {
19-
/// An `UndoManager` subclass that forwards relevant actions to a `CEUndoManager`.
20-
/// Allows for objects like `TextView` to use the `UndoManager` API
21-
/// while CETV manages the undo/redo actions.
22-
public class DelegatedUndoManager: UndoManager {
23-
weak var parent: CEUndoManager?
24-
25-
public override var isUndoing: Bool { parent?.isUndoing ?? false }
26-
public override var isRedoing: Bool { parent?.isRedoing ?? false }
27-
public override var canUndo: Bool { parent?.canUndo ?? false }
28-
public override var canRedo: Bool { parent?.canRedo ?? false }
29-
30-
public func registerMutation(_ mutation: TextMutation) {
31-
parent?.registerMutation(mutation)
32-
removeAllActions()
33-
}
34-
35-
public override func undo() {
36-
parent?.undo()
37-
}
38-
39-
public override func redo() {
40-
parent?.redo()
41-
}
42-
43-
public override func beginUndoGrouping() {
44-
parent?.beginUndoGrouping()
45-
}
46-
47-
public override func endUndoGrouping() {
48-
parent?.endUndoGrouping()
49-
}
50-
51-
public override func registerUndo(withTarget target: Any, selector: Selector, object anObject: Any?) {
52-
// no-op, but just in case to save resources:
53-
removeAllActions()
54-
}
55-
}
56-
18+
public class CEUndoManager: UndoManager {
5719
/// Represents a group of mutations that should be treated as one mutation when undoing/redoing.
5820
private struct UndoGroup {
5921
var mutations: [Mutation]
@@ -65,16 +27,17 @@ public class CEUndoManager {
6527
var inverse: TextMutation
6628
}
6729

68-
public let manager: DelegatedUndoManager
69-
private(set) public var isUndoing: Bool = false
70-
private(set) public var isRedoing: Bool = false
30+
private var _isUndoing: Bool = false
31+
private var _isRedoing: Bool = false
7132

72-
public var canUndo: Bool {
73-
!undoStack.isEmpty
74-
}
75-
public var canRedo: Bool {
76-
!redoStack.isEmpty
77-
}
33+
override public var isUndoing: Bool { _isUndoing }
34+
override public var isRedoing: Bool { _isRedoing }
35+
36+
override public var undoCount: Int { undoStack.count }
37+
override public var redoCount: Int { redoStack.count }
38+
39+
override public var canUndo: Bool { !undoStack.isEmpty }
40+
override public var canRedo: Bool { !redoStack.isEmpty }
7841

7942
/// A stack of operations that can be undone.
8043
private var undoStack: [UndoGroup] = []
@@ -93,10 +56,7 @@ public class CEUndoManager {
9356

9457
// MARK: - Init
9558

96-
public init() {
97-
self.manager = DelegatedUndoManager()
98-
manager.parent = self
99-
}
59+
override public init() { }
10060

10161
convenience init(textView: TextView) {
10262
self.init()
@@ -106,37 +66,49 @@ public class CEUndoManager {
10666
// MARK: - Undo/Redo
10767

10868
/// Performs an undo operation if there is one available.
109-
public func undo() {
110-
guard !isDisabled, let item = undoStack.popLast(), let textView else {
69+
override public func undo() {
70+
guard !isDisabled, let textView else {
71+
return
72+
}
73+
74+
guard let item = undoStack.popLast() else {
75+
NSSound.beep()
11176
return
11277
}
113-
isUndoing = true
114-
NotificationCenter.default.post(name: .NSUndoManagerWillUndoChange, object: self.manager)
78+
79+
_isUndoing = true
80+
NotificationCenter.default.post(name: .NSUndoManagerWillUndoChange, object: self)
11581
textView.textStorage.beginEditing()
11682
for mutation in item.mutations.reversed() {
11783
textView.replaceCharacters(in: mutation.inverse.range, with: mutation.inverse.string)
11884
}
11985
textView.textStorage.endEditing()
120-
NotificationCenter.default.post(name: .NSUndoManagerDidUndoChange, object: self.manager)
86+
NotificationCenter.default.post(name: .NSUndoManagerDidUndoChange, object: self)
12187
redoStack.append(item)
122-
isUndoing = false
88+
_isUndoing = false
12389
}
12490

12591
/// Performs a redo operation if there is one available.
126-
public func redo() {
127-
guard !isDisabled, let item = redoStack.popLast(), let textView else {
92+
override public func redo() {
93+
guard !isDisabled, let textView else {
94+
return
95+
}
96+
97+
guard let item = redoStack.popLast() else {
98+
NSSound.beep()
12899
return
129100
}
130-
isRedoing = true
131-
NotificationCenter.default.post(name: .NSUndoManagerWillRedoChange, object: self.manager)
101+
102+
_isRedoing = true
103+
NotificationCenter.default.post(name: .NSUndoManagerWillRedoChange, object: self)
132104
textView.textStorage.beginEditing()
133105
for mutation in item.mutations {
134106
textView.replaceCharacters(in: mutation.mutation.range, with: mutation.mutation.string)
135107
}
136108
textView.textStorage.endEditing()
137-
NotificationCenter.default.post(name: .NSUndoManagerDidRedoChange, object: self.manager)
109+
NotificationCenter.default.post(name: .NSUndoManagerDidRedoChange, object: self)
138110
undoStack.append(item)
139-
isRedoing = false
111+
_isRedoing = false
140112
}
141113

142114
/// Clears the undo/redo stacks.
@@ -147,11 +119,17 @@ public class CEUndoManager {
147119

148120
// MARK: - Mutations
149121

122+
public override func registerUndo(withTarget target: Any, selector: Selector, object anObject: Any?) {
123+
// no-op, but just in case to save resources:
124+
removeAllActions()
125+
}
126+
150127
/// Registers a mutation into the undo stack.
151128
///
152129
/// Calling this method while the manager is in an undo/redo operation will result in a no-op.
153130
/// - Parameter mutation: The mutation to register for undo/redo
154131
public func registerMutation(_ mutation: TextMutation) {
132+
removeAllActions()
155133
guard let textView,
156134
let textStorage = textView.textStorage,
157135
!isUndoing,
@@ -178,15 +156,15 @@ public class CEUndoManager {
178156
// MARK: - Grouping
179157

180158
/// Groups all incoming mutations.
181-
public func beginUndoGrouping() {
159+
override public func beginUndoGrouping() {
182160
guard !isGrouping else { return }
183161
isGrouping = true
184162
// This is a new undo group, break for it.
185163
shouldBreakNextGroup = true
186164
}
187165

188166
/// Stops grouping all incoming mutations.
189-
public func endUndoGrouping() {
167+
override public func endUndoGrouping() {
190168
guard isGrouping else { return }
191169
isGrouping = false
192170
// We just ended a group, do not allow the next mutation to be added to the group we just made.

Tests/CodeEditTextViewTests/TextViewTests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,18 @@ struct TextViewTests {
6464
#expect(textView1.layoutManager.lineCount == 3)
6565
#expect(textView2.layoutManager.lineCount == 3)
6666
}
67+
68+
@Test("Custom UndoManager class receives events")
69+
func customUndoManagerReceivesEvents() {
70+
let textView = TextView(string: "")
71+
72+
textView.replaceCharacters(in: .zero, with: "Hello World")
73+
textView.undo(nil)
74+
75+
#expect(textView.string == "")
76+
77+
textView.redo(nil)
78+
79+
#expect(textView.string == "Hello World")
80+
}
6781
}

0 commit comments

Comments
 (0)