Undo redux: NSTextView/NSUndoManager badly broken (really)
Undo redux: NSTextView/NSUndoManager badly broken (really)
- Subject: Undo redux: NSTextView/NSUndoManager badly broken (really)
- From: mmark <email@hidden>
- Date: Sat, 20 Oct 2001 19:41:18 -0700
Hi everybody,
This message is so long that I offer a table of contents for your
convenience (sorry...):
1) Summary
2) The Goal (my desired undo behavior - not too relevant)
3) The Ape-Man Test (how to get the bug)
4) Investigating the problem (steps I took)
5) TextEdit and Project Builder vs. the Ape-Man (other apps break, too)
6) This is a Pretty Serious Bug
7) How You Can Help
8) Now What?
##### SUMMARY #####
I have found a serious bug with Cocoa's built in text editing undo/redo
behavior, that not only breaks my app, but breaks many existing apps.
I'd like to make sure that other people can reproduce it, and get any
suggestions anybody has.
##### THE GOAL #####
I've spent several full days debugging problems with Undo in our app.
(Readers of the MacOSX-Dev list might have caught the discussion there on
Aug 27-28.) The basic gist of my problem is that we need to have unlimited
undo of text editing in our app.
An added complexity in our app is that the editing takes place in a kind of
form, which has several text editing fields (NSTextView). When an edit field
has focus, the standard "Undo Typing"/"Undo Cut" undo actions are available,
but when the focus leaves the field, then all those little undoable actions
are replaced with a single action, such as "Undo Edit XXXX" where XXX is the
name of the field being edited.
I implemented a suggestion from Greg Titus to get that kind of behavior
(available in the MacOSX-Dev archives for 8/28/2001 if you're interested).
At first, I thought it worked fine. However, when I subjected it to the
"Ape-Man Test" (described below), it broke down completely.
##### THE APE-MAN TEST #####
Testing an undo implementation is difficult without user interaction
automation tools. Code that appears to work fine might break under uncommon
conditions. So, I applied the "ape-man test" which basically involves
opening a document window and banging like an ape on the keyboard while
spastically clicking the mouse within the text view, for 20-30 seconds. To
do this test, you must be sure to type lots of characters, including delete,
while making sure that your spastic mouse actions change the selection
frequently and select ranges of text for typeover/delete. Then you hold down
Command-Z to undo back to the beginning, then Command-Shift-Z to redo back
to the last state. Then you repeat.
Our app failed this test.
During the ape-man test, we'd get an error like this:
2001-10-20 17:36:45.601 Boogie Templates[2321] -[NSConcreteTextStorage
attributedSubstringFromRange:]: Invalid range {8, 3} for string of length 8
Then, the NSUndoManager in charge of undo would stop working, and raise an
exception:
2001-10-20 18:18:18.019 Boogie Templates[2321] undo: NSUndoManager
0x2d575c0 is in invalid state, undo was called with too many nested undo
groups
The second exception is caused by the first, but the result is that Undo and
Redo are now totally broken, and neither Undo nor Redo is possible.
Obviously, this could potentially have very serious consequences for the
user if they had undone a substantial amount of work. In short, the app is
badly broken.
##### INVESTIGATING THE PROBLEM #####
This was really, really hard--mainly, our app seemed to work. However, the
code seemed to break in random places, and I could not come up with a single
reproducible step-by-step case. The ape-man test did always break the app,
but the ape-man test by its nature is virtually impossible to reproduce the
same way twice.
I thought it must be something we were doing wrong with our form-field
editing, abusing NSUndoManager somehow. I tore up our code and
re-implemented it in a simple window with minimal functionality. This is
probably why I wasted so much time, because I now think my assumption was
incorrect. I no longer think our code is doing anything wrong.
I set a gdb breakpoint on -[NSException raise] and caught the first
exception right after it occurred (in the middle of pressing Cmd-Z a few
times), then did a trace:
(gdb) bt
#0 0x708788a0 in -[NSException raise] ()
#1 0x70852a50 in +[NSException raise:format:arguments:] ()
#2 0x708529ac in +[NSException raise:format:] ()
#3 0x70ea987c in -[NSTextStorage(NSSubstringHack)
attributedSubstringFromRange:] ()
#4 0x70e72dc0 in -[NSUndoTyping undoRedo:] ()
#5 0x70e56aa8 in -[NSTextStorage(NSUndo) _undoRedoTextOperation:] ()
#6 0x708b6ac0 in -[_NSUndoLightInvocation invoke] ()
#7 0x708b7174 in -[_NSUndoStack popAndInvoke] ()
#8 0x708b14b0 in -[NSUndoManager undoNestedGroup] ()
#9 0x708b1644 in -[NSUndoManager undo] ()
#10 0x70bc8290 in -[NSWindow undo:] ()
#11 0x70833b28 in -[NSObject performSelector:withObject:] ()
#12 0x70c94698 in -[NSApplication sendAction:to:from:] ()
#13 0x70bf9d7c in -[NSMenu performActionForItemAtIndex:] ()
#14 0x70c1d638 in -[NSCarbonMenuImpl
performActionWithHighlightingForItemAtIndex:] ()
#15 0x70c7c3c4 in -[NSMenu performKeyEquivalent:] ()
#16 0x70b943a4 in -[NSApplication sendEvent:] ()
#17 0x70c23488 in -[NSApplication run] ()
#18 0x70c91ed0 in NSApplicationMain ()
#19 0x0000ff70 in main (argc=1, argv=0xbffffbd4) at
Source/App_Document_Etc/main.m:5/Volumes/mason/Projects/Hayaku/
#20 0x00003d04 in _start ()
#21 0x00003b34 in start ()
I was surprised that this whole stack didn't show any of our methods--I had
assumed that one of our app's undoable actions would be involved in
producing the problem. But I wondered now if this was even my bug at all. I
particularly wondered about NSTextStorage(NSSubstringHack).
Still, I thought, maybe the field-editing undo behavior we were implementing
was somehow putting things in a bad state. If our app is the only place this
problem manifested itself, then it is our bug, regardless of whose code is
executing when it happens.
So, I decided to test some other apps.
##### TEXTEDIT & PROJECT BUILDER VS. THE APE MAN #####
I ran the ape-man test against a new document in TextEdit, and watched the
console:
2001-10-20 17:40:55.744 TextEdit[385] -[NSConcreteTextStorage
attributedSubstringFromRange:]: Invalid range {55, 31} for string of length
85
2001-10-20 17:40:55.774 TextEdit[385] undo: NSUndoManager 0x2805e30 is
in invalid state, undo was called with too many nested undo groups
Damn it! I smashed my coffee mug against the wall for dramatic effect, then
ran the ape-man test against a brand new text document in Project Builder:
2001-10-20 17:57:31.758 Project Builder[383] -[PBXTextStorage
attributedSubstringFromRange:]: Invalid range {0, 53} for string of length
52
2001-10-20 17:57:31.776 Project Builder[383] undo: NSUndoManager
0x11d110 is in invalid state, undo was called with too many nested undo
groups
I had that sweet-and-sour realization that Hey, maybe this is not my fault,
combined with Hey, maybe I've wasted several days barking up the wrong tree,
and maybe it's not my fault but I still have to fix it.
Furthermore, I decided that our field-editing undo stuff was irrelevant--the
bug happens inside of single NSTextView without changing the first
responder.
##### THIS IS A PRETTY SERIOUS BUG #####
The ape-man test, although somewhat extreme, is a valid test. It consists
solely of the following user actions:
- type new text
- type over selected text
- delete selected text
- delete characters
- change selection
Also, it is not strictly necessary to perform the ape-man test for 30
seconds; occasionally, I was able to break things in just a few seconds.
I think it is uncommon to encounter this problem as a user. I did not notice
the problem until I started stress-testing our app. However, in retrospect,
I DO think that this has happened to me once or twice in Project Builder. At
the time I just probably chalked it up to PB being a bit flaky.
To the end user, what happens when you run into this bug is that the Undo
menu just stops working. It says something like "Undo Typing" and "Redo
Typing", but choosing the menu commands has no effect. It is irritating (and
potentially enraging) but the app keeps going, and new documents are
generally not affected until they also hit the bug, which probably doesn't
happen.
The reasons I think this is a very important bug are:
a) it's breaking my app and making my life hell
b) (more seriously) it is bad and seems to affect a LOT of apps
As near as I can tell, any application that uses NSTextView, NSUndoManager,
and the built in text editing undo behavior will be negatively affected by
this bug...and that is probably a lot of apps.
##### HOW YOU CAN HELP #####
A) Reverse engineer NSTextView and NSTextStorage, fix the bug, make them
poseAsClass, package it up in a framework, and send it to me along with a
royalty free distribution license.
B) Um...maybe you could see if you can reproduce the bug?
I'd like to get a bit more information (if possible) before I file a bug
report; a step-by-step reproducible case would be really great. So far, I
haven't been able to get one.
I'd also like to confirm that other people can make this bug happen,
although I doubt there is anything specific to my machine causing this
misbehavior. I am running 10.1 on a dual processor G4 with nothing funky
installed. (It would be great if I remembered that I installed Super Text
Editing Hack 0.1a1 last week, and removing that fixed the problem. But I
didn't.)
##### AND NOW WHAT? #####
Finally, I am faced with the question of what to do. We have a deadline
coming up (November). As I see it, I have three options:
A) Ship the product using the broken Undo API, and hope that an OS update
will fix the bug and my product will start to work reliably then.
B) Eliminate all use of the system-supplied undo behavior and implement all
undo behavior myself.
C) Figure out a clever workaround.
Option A is probably actually out of the question. Even though the bug is
somewhat rare, it does happen, and the results are bad enough that I don't
feel comfortable shipping with that bug lurking in there.
Option B sucks because it is a lot of work, it requires extensive debugging
and beta testing (to be sure I don't have my own bug similar to this
existing one), and it might raise some inconsistencies in text editing
behavior between my app and apps using the built-in behavior. Text editing
(and associated undo/redo) is one of those things that really should be
consistent.
Option C would be great...but I haven't figured out any clever workaround.
So, to the three people who have actually read this far:
- Can you reproduce this problem?
- Do you have any suggestions?
Thanks a lot (really!),
--
Mason Mark
Five Speed Software, Inc.