Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature-Request: Restorable undoManager (or even restorable session!) #1452

Closed
Chris78 opened this issue May 28, 2013 · 11 comments
Closed

Feature-Request: Restorable undoManager (or even restorable session!) #1452

Chris78 opened this issue May 28, 2013 · 11 comments

Comments

@Chris78
Copy link

Chris78 commented May 28, 2013

Hi,

it would be cool, if the state of the current undoManager could be stored and restored later, even after leaving the page.

I once implemented such a persistent undo/redo history for textareas. I used undo/redo stacks with a max. number of say n deltas. On submission of the form I stored both delta-stacks (using session storage) referenced by a unique key for that textarea.

Returning to the page later I could restore the undo/redo stacks from the data in session storage.

I would like to adopt this behavior into the ACE Editor, but I failed saving and restoring the state of the undoManager (using Object.toJSON() and JSON.parse()), because of the following reasons:

  • undoManager().$doc cannot be serialized (continuous loop) but is apparently needed to restore a working undoManager
  • the Range-objects (deep inside the deltas) loose their class "Range" during Object.toJSON() and are degraded to the generic class "Object", so the undoManager fails to call clone() and other methods on those objects.

Now, it would be cool to have something like session.getUndoManager().toString() and session.getUndoManager.loadFromString(str) or even save/load methods that store the data in some (configurable) place (i.e. sessionStorage, localStorage).

Or even one step further: session.save()->String and session.load(str).

Regards
Chris

@jpillora
Copy link

jpillora commented Dec 5, 2013

👍 for [de]serializing undo manager

@ilkovich
Copy link

👍

1 similar comment
@adrianbj
Copy link
Contributor

👍

@MrMoronIV
Copy link

Has this ever been implemented? Would like to store the undomanager in localstorage... 🙂

@vanillajonathan
Copy link
Contributor

vanillajonathan commented Jun 18, 2019

Link of interest: https://stackoverflow.com/a/53387989/2470524

@vanillajonathan
Copy link
Contributor

vanillajonathan commented Jun 19, 2019

EditSession could also provide its own toJSON method to provide serialization functionality.
It currently does not provide such a method, but you can extend EditSession using prototypes.

ace.EditSession.prototype.toJSON = function() {
    return {
        annotations: this.getAnnotations(),
        breakpoints: this.getBreakpoints(),
        folds: this.getAllFolds().map(function(fold) {
            return fold.range;
        }),
        history: {
            undo: this.getUndoManager().$undoStack,
            redo: this.getUndoManager().$redoStack,
            rev: this.getUndoManager().$rev,
            mark: this.getUndoManager().mark
        },
        mode: this.getMode().$id,
        scrollLeft: this.getScrollLeft(),
        scrollTop: this.getScrollTop(),
        selection: this.getSelection().toJSON(),
        value: this.getValue()
    };
};

This lets you persist the session:

let session = JSON.stringify(editor.getSession());
localStorage.setItem('session', session);

@MrMoronIV
Copy link

That looks nice, how would one restore the information again after the browser has been reloaded?

@vanillajonathan
Copy link
Contributor

vanillajonathan commented Jun 20, 2019

That looks nice, how would one restore the information again after the browser has been reloaded?

Get the serialized session from the storage and deserialize it back into a object:

let session = JSON.parse(localStorage.getItem('session'));

Then either create a new EditSession object and modify that. I think that would be something like this.

function createEditSession(session) {
    let editSession = new ace.EditSession(session.value);
    session.folds.forEach(function(fold) {
        editSession.addFold("...", ace.Range.fromPoints(fold.start, fold.end));
    });
    editSession.setAnnotations(session.annotations);
    editSession.setBreakpoints(session.breakpoints);
    editSession.setUndoManager(new ace.UndoManager());
    editSession.$undoManager.$undoStack = session.history.undo;
    editSession.$undoManager.$redoStack = session.history.redo;
    editSession.$undoManager.$rev = session.history.rev;
    editSession.$undoManager.mark = session.history.mark;
    editSession.setMode(session.mode);
    editSession.setScrollLeft(session.scrollLeft);
    editSession.setScrollTop(session.scrollTop);
    editSession.selection.fromJSON(session.selection);
    return editSession;
}

let editSession = createEditSession(session);
editor.setSession(editSession);

Or you could just modify the existing default session without creating a new EditSession object. I think that would be something like this.

session.folds.forEach(function(fold) {
    editor.session.addFold("...", ace.Range.fromPoints(fold.start, fold.end));
});
editor.session.setAnnotations(session.annotations);
editor.session.setBreakpoints(session.breakpoints);
editor.session.$undoManager.$undoStack = session.history.undo;
editor.session.$undoManager.$redoStack = session.history.redo;
editor.session.$undoManager.$rev = session.history.rev;
editor.session.$undoManager.mark = session.history.mark;
editor.session.setMode(session.mode);
editor.session.setScrollLeft(session.scrollLeft);
editor.session.setScrollTop(session.scrollTop);
editor.session.selection.fromJSON(session.selection);
editor.setValue(session.value);

Edit: I've updated this comment a couple of times. Note that folds are n-depth so needs a recursive function to properly restore.

@Chris78
Copy link
Author

Chris78 commented Jun 21, 2019

Update 1: :-( I could restore the undoStacks and can even undo after page reload, but now I get
TypeError: this.$informUndoManager is undefined
when I try to type in the textarea...


Update 2: I could fix the above issue by replacing
editSession.$undoManager=new ace.UndoManager();
with
editSession.setUndoManager(new ace.UndoManager());
in the createEditSession() function.


Update 3: Next fix (and sorry for spamming everybody again):
Do not create a new editSession on restore. Just apply the stored data to the existing editSession like this:

function restoreEditSession(editor, sessiondata) {
  editSession = editor.getSession();
  editSession.setAnnotations(sessiondata.annotations);
  editSession.setBreakpoints(sessiondata.breakpoints);
  editSession.setUndoManager(new ace.UndoManager());
  editSession.$undoManager.$undoStack = JSON.parse(sessiondata.history.undo);
  editSession.$undoManager.$redoStack = JSON.parse(sessiondata.history.redo);
  editSession.setMode(sessiondata.mode);
  editSession.setScrollLeft(sessiondata.scrollLeft);
  editSession.setScrollTop(sessiondata.scrollTop);
  editSession.selection.fromJSON(sessiondata.selection);
}

sessiondata = JSON.parse(localStorage.getItem('session'));
editor = editors.my_textarea;
restoreEditSession(editor, sessiondata);

I had to make a few minor changes for @vanillajonathan 's code to work for me.

Here is my version of the session restoration:

1.) Store the session and undoManager-data (just as proposed by vanillajonathan):

editor = editors.my_textarea;

ace.EditSession.prototype.toJSON = function() {
    return {
        annotations: this.getAnnotations(),
        breakpoints: this.getBreakpoints(),
        folds: this.getAllFolds().map(function(fold) {
            return fold.range;
        }),
        history: {
            undo: this.getUndoManager().$undoStack,
            redo: this.getUndoManager().$redoStack
        },
        mode: this.getMode().$id,
        scrollLeft: this.getScrollLeft(),
        scrollTop: this.getScrollTop(),
        selection: this.getSelection().toJSON(),
        value: this.getValue()
    };
};

let session = JSON.stringify(editor.getSession());
localStorage.setItem('session', session);

2.) To restore after page reload (I tried to highlight my changes in bold, so I couldn't insert it as a code block):

function createEditSession(session) {
editSession = new ace.EditSession(session.value);
editSession.setAnnotations(session.annotations);
editSession.setBreakpoints(session.breakpoints);
editSession.setUndoManager(new ace.UndoManager());
editSession.$undoManager.$undoStack = JSON.parse(session.history.undo);
editSession.$undoManager.$redoStack = JSON.parse(session.history.redo);
editSession.setMode(session.mode);
editSession.setScrollLeft(session.scrollLeft);
editSession.setScrollTop(session.scrollTop);
editSession.selection.fromJSON(session.selection);
return editSession;
}

session = JSON.parse(localStorage.getItem('session'));
editSession = createEditSession(session);
editor = editors.my_textarea;
editor.setSession(editSession);

@github-actions
Copy link

github-actions bot commented Mar 5, 2023

This issue has received a significant amount of attention so we are automatically upgrading its priority. A member of the community will see the re-prioritization and provide an update on the issue.

NotLazy added a commit to NotLazy/ace that referenced this issue Jul 4, 2023
InspiredGuy pushed a commit that referenced this issue Oct 11, 2023
* Implement #1452

* fix errors

* Add comments to modified functions

* Add tests

* Add typescript definitions
@InspiredGuy
Copy link
Contributor

#5236 and #5326 were merged, which provide toJSON and fromJSON methods for both UndoManager and EditSession, which should help with serialization. This will be available in the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants