Improve the Undo?

Hi. First, let me say that 2.0 is very very nice. A lot of hard work has produced a lot of very nice improvements.

Now for the feedback :slight_smile:

Undo is very limited. Reorganising documents can’t be undone, so accidental drops need be manually fixed. Renaming documents cannot be done either. A consistent undo would definitely add to the product. (Check out something like Omnioutliner – everything is undoable!).

But, again, great product!

The big difference between a program like Scrivener and OmniOutliner is that the former is managing files and the latter is not, so it isn’t so easy to compare to two. Some of these file-based operations would be very difficult, and maybe not even possible, to handle with undo. Consider split and merge, and the potential for making changes within the documents that disturbs the old seam between the files before attempting undo. A large part of the difficulty is that Scrivener is not a single-undo application—in part because of its complex nature. There isn’t just one global undo stack, but rather each component of the interface has its own undo. Two different files operate independently from each other, let alone their synopsis and notes fields, to give an example—so all of these higher level functions would be on their own undo track, and not keeping tabs on the state of the internal file changes.

That’s just a few of the challenges, and why things are the way they are.

Hm. Not sure what to say here.

If I put on my user hat:

  • Undoing of splits and merges is exactly what I want.

If I put on my software developer hat I would say this:

  • The fact that Scrivener writes and manages documents as individual files is an implementation detail. It doesn’t reflect how it is (or at least could be) be modelled internally. (consider: if the omni developers decided to write omnioutliner documents so that each bullet was written as a separate rtf document, I suspect they could do this very easily).

If I put on my realists hat:
I guess we ain’t going to see a consistent global undo any time soon.

Oh well. keep up the good work.

It is an implementation detail. Scrivener does a great job of hiding the reality on nearly every front. Hardly anyone knows (and of those that do, hardly any of them think about it) that whenever you click on a binder item, you are loading anywhere from zero to four files off of the hard drive. They are all getting merged into a single presentation in the Inspector and main text editor. When you merge three files together, you might be merging up to twelve individual files. You can imagine how troublesome it would be to undo a potentially no longer identical complex of twelve files out of four.

The fact that Omni could switch to RTF files for every action item is true, but it would also be unwise for a program that stores data chiefly as terse one-liners. The same is true of Scrivener. It could be redesigned as a program that uses one huge XML file for all of its data—but that would be supremely unwise given that it supports the notion of allowing authors to use the project as a data clearinghouse. You’d have to nix the whole idea of letting people store as many PDFs and movies as they want, because save times would skyrocket, just to mention one negative.

So while it is an implementation detail, it is also the sanest approach to the data model that Scrivener encourages. XML is the sanest approach to the data model that OmniFocus encourages. Doing away with an arguably vastly superior method to accommodate something like file-level undo which has reasonable mirror commands for 95% of what can be done would be, to reiterate, unwise. It would solve the undo problem: but it would severely limit Scrivener’s usefulness in key areas where it is well known for excelling above many other writing programs out there.

In short: I think your realist hat is a good one. :slight_smile:

Actually it might help the undo problem but it wouldn’t solve it. OmniOutliner isn’t comparable because you’re always dealing with a single document in the view (yes, I know there’s hoist etc but it’s still one document). Scrivener deals with different documents, and it is mainly based on the Cocoa text view for handling text. It uses the Cocoa undo manager, which operates at the view-level rather than the model-level (a design decision taken by Apple, not me). So, without completely rewriting the Cocoa undo system…

Hm. I’m not sure we’re thinking in the same way.

There are two very different things which you seem to clump together: 1. how a program deals with its data in memory and 2. how it stores it on disk. These do not need to be the same. What matters for undo (unless you are trying for persistent undo) is how stuff is done in memory. Omnioutliner, for all we know, might store its outline document as lots of tiny RTF strings in memory, but then save it out as XML. The point is this: just because Scriver writes out RTF files need place no constraints on how it manipulates that data in memory (though it seems from how you are writing that it does…?)

I also don’t see why splitting and merging is such a problem if you maintain a single undo stack (ie, you can’t selectively undo certain bits of history): Let’s say you’re merging three documents (NOT files, those are the things on disk). All you do is take a copy of all 3 docs and a copy of the current hierarchy (it’s text, and memory is cheap) and stick it on the undo stack. Any subsequent editing of the merged document will have had to be undone by the time you undo to this level of the stack, so undoing is simply a case of removing the merged document, and replacing it with the copies you made. Sure, it’s harder than that in practice, but in principle there does not seem to be anything that complex (certainly not impossible) about this.

I’m harping on about this because I think the scrivening ability is so cool. But the illusion of having one document is lost because any multidocument re-organisation cannot be undone.

That said, I’ve still got my realists hat on.

… I should add, I’ve done almost no Cocoa programming, so perhaps I’m just not seeing the “Cocoa” way of doing things…

Several problems, Cocoa aside:

  1. These actions are not in memory. When you split, files are immediately created on the disk and saved. This is true for all “file level” actions. In fact, very little in Scrivener is truly purely in memory and not on disk, because of the short auto-save interval. Even if it were just in memory though, it still wouldn’t negate the complexity of the problem. Even if the buffered files were not on the disk yet, you still have the problem of them all having their own undo stacks.
  2. So to combat that, you propose unifying the undo stack (setting aside for a moment how much work that would be, as Keith points out, it would be a huge project to implement that). I don’t think that’s a good sacrifice for the same reason I don’t think changing to XML is a good sacrifice. In both cases, you are sacrificing a huge advantage for something which, in my opinion anyway, is pretty trivial considering the mirror commands. Split and Merge are the absolute worst of the lot. Everything else is very easily undone with a mirror command (or difficult to accidentally do). I make accidental new files all of the time, I’m just twitchy or something. Cmd-Deleting them is essentially just as easy as Cmd-Z. Removing the multi-stack aspect of Scrivener means removing a huge safety net. That safety net makes it possible to slap your forehead two hours later when you realised an edit in a document was wrongly applied. You just switch back, and without even having to find that spot, hit Cmd-Z. Done. If there was a unified undo stack, you would pretty much have to resign yourself to the fact that you would need to manually unravel whatever that edit was by hand, because stepping back through 500 actions in the queue would be (a) unnecessarily destructive to all of the recent changes you’ve made and (b) a waste of time.

So yes, a unified stack would remove the near impossibility of undoing a merge in a complex environment since any changes to the “seams” of the old files would be restored via a succession of undos—but really—that’s a lot to sacrifice of the program, and it presumes all of those edits you made in between are of less value than the merge action for all cases. That might be so for some cases, but I would wager it wouldn’t always be the case. I bet most of the time subsequent edits (many of which probably won’t even be remembered clearly after done) would be of equal or greater value, and so manually splitting things back apart would be better.

This is a worst case scenario. This is the worst position you can put Scrivener in: a poorly reasoned Merge you don’t realise until half an hour later. Split is slightly less problematic in that it requires a bit less of the user in sorting out their mistake (and even then this contrived example must be pushed a little further to an example where there are lots of meta-data records to re-split back up; a straight, simple accidental merge is very easy to fix). Everything else. Everything, is easy to manually undo. So focussing on this one nasty case isn’t really a fair assessment. The bulk of these file level undos would be for silly stuff like empty Untitled files or re-ordering files. All stuff you can easily fix on your own, and its so easy to fix because its easy to do them in the first place.

Yeah, it’s really where Scrivenings come into play that this implementation is most obvious and not quite as easy to undo manually (and you can’t even merge in scrivenings, anyway). Scrivenings makes the multi-file editing thing so slick, any reminder that you are working in files can be a bit jarring. Even so, I don’t think sacrificing project research capacity or fine-granularity undo would be good moves at all. It swaps a mildly jarring “oops” for a far less powerful application that really only feasibly stores text, and gets really slow once a book hits a reasonably normal size, or one that greatly reduces its undo power which, it is very key to note, scales with the complexity of your project seamlessly. Unified stack would not scale at all, and it would artificially constrict people into larger data chunks to work around the 500-undo-ago “damn it!” problem that would be, I would wager, a frequent curse were it to function that way.

Well, the first thing you’re missing is that a Scrivener “document” has several components: the text itself, the index card metadata (title, synopsis, etc.), the outliner metadata (status, labels, word count target, etc.), and the reference metadata (document notes, keywords, etc.). All of those have to travel through the merge/undo merge process together, making the management task much more complicated.

The second thing that you’re missing is that the user may not want a single undo stack. Suppose I edit the text of a document, then edit its synopsis, then edit the text some more, then decide I want to undo the synopsis change. In the current design, the synopsis and the text have separate undo stacks, so this is possible. In your design, it wouldn’t be. For my use, at least, that’s a much more common scenario than undoing a document merge. (Remember that a temporary document “merge” is trivially easy, via Edit Scrivenings, so I only merge things when I’m really sure.)

And finally, remember that one of Scrivener’s imperatives is to not lose user work, and so it autosaves everything very frequently: every few seconds, by default. Any memory construct you create will be mapped to files on disk in very short order. Furthermore, a second imperative is that the files be sufficiently readable that the user can retrieve their work even if Scrivener itself no longer exists: the file format must be user-parseable. So it’s sort of ducking the question to pretend that documents can be so easily isolated from their associated files.


I don’t really have time to go into the various technical difficulties right now as that would be very long and drawn out, but trust me, I’m not withholding undo in these areas out of negligence or mean-spiritedness. Undo in Cocoa is a very tricky business, especially when data is swapped in and out of various views and is only loaded lazily from disk. You’ll find similar issues in programs that deal with data in similar ways, such as Ulysses (OmniOutliner is not comparable).

All the best,

Thanks for the many replies.

The main point I see now is that the use of have undo on separate files, rather than a global undo. I guess I just presumed that undo should work at a global level, because I spend so much time in scrivening mode. But as has been pointed out, there are lots of advantages to it NOT working that way. I guess I just have to think of the scrivening mode as “special” in some sense, and think of scrivener as a multi file editor (like I use for programming).

Keith: I love the program, and never considered for a second you were being mean-spirited. Please don’t take my comments as negative. I’m here in the forum cos’ I think Scrivener is well worth the time trying to contribute suggestions, bug reports etc.

No, I agree with you that it would be much better if undo could work globally. Undo in Cocoa is a bit of a mess though. You would think that undo would work on the underlying data model and then reflect any changes to the undo stack in the interface, but it doesn’t work that way; for some reason, in Cocoa, undo is maintained by the interface itself, and by the individual components of the interface. That’s fine if you just have one main view on your data - a single TextEdit document for instance - but when much of the data isn’t even loaded into a view at all (as in Scrivener), it’s a massive problem. I know Jesse Grosjean pretty much wrote his own undo system for TaskPaper, that could operate on the model instead of the interface - I dread to think what that would involve for Scrivener!

All the best,

It does seem odd that undo works at the View level. I seem to remember similar problems working in the Microsoft’s MFC framework: it did lots for you, but bits of it just didn’t work the way you expected. Oh well…

The strange thing is, in dealing with a crash report yesterday, I noticed that NSTextStorage - which is Cocoa’s model layer for the text that appears in a text view - actually deals with the undo “behind the scenes”, in a private area of Cocoa unavailable to developers. So the text view must be sending the undo stuff to the model layer after all; that is, it seems that the model layer may well handle the undo in this one case, but Cocoa is designed such that the developer interacts with it through the view layer. Weird. I should add that I’m mainly talking about text view interactions here, too. In other code it’s more common to have undo stuff in the controller layer. But because Scrivener’s editor is built on the text system, I rely on NSTextViews’ undo and would need to write my own undo code for the text system to get around it, which would not be pretty, because it could seriously mess up NSTextView’s expectations.

All the best,