Text Editor with Undo
Build the core of a text editor with undo and redo. Every edit is an action that can be reversed, and a user can also bundle several edits into one step, the way a "replace all" or a macro is undone with a single Ctrl+Z. This is the Command pattern: each action is an object that knows how to do and undo itself, so the editor needs no special case per kind of edit.
Implement the class Editor. It starts with the empty text "" and an empty history. Text is plain ASCII, and the cursor is always at the end.
Editing
type(text): appendstext. Returns the number of characters typed. Typing""does nothing, returns 0 and is not recorded.delete(count): removes up tocountcharacters from the end. Deleting more than exist removes what exists. Returns the number of characters actually removed. Ifcountis below 1 or the text is empty, nothing happens, it returns 0 and it is not recorded.
History
undo(): reverses the most recent recorded action and returnstrue, or does nothing and returnsfalsewhen there is nothing to undo. Undoing adeleterestores exactly the characters that delete removed, which is not alwayscount(see the note).redo(): re-applies the most recently undone action and returnstrue, or returnsfalsewhen there is nothing to redo.canUndo()andcanRedo(): whetherundo()orredo()would do something.text(): the current text.- A new action clears the redo history. Recording any action (a
typeordeletethat changes something, or a closed group that contains something) discards everything that could have been redone.
Groups
beginGroup(): starts a group. Everything done until the matchingendGroup()becomes one undo step. It returns nothing.endGroup(): ends the innermost open group. Returnsfalseif no group is open, otherwisetrue.- Groups nest, and a nested group collapses into the outermost one: only the
endGroup()that closes the outermost group records the step. - A group that ends up with no recorded actions (nothing inside, or only no-op actions such as
type("")) is not recorded, and does not clear the redo history. - Inside an open group the edits apply to
text()immediately, but the history is frozen:undo()andredo()returnfalsewithout doing anything, andcanUndo()andcanRedo()returnfalse. A group that is never closed is never recorded. - Undoing a group reverses its actions in the opposite order. Redoing it replays them in the original order.
Notes
- Clamping example: after
type("ab"),delete(5)returns2and leaves"". Undo restores"ab"(the 2 characters removed), not 5 characters. - A recorded action is exactly one undo step, so
type("hello")is undone in one go, not letter by letter. - There are no exceptions: every call returns one of the values above.
Examples
Example 1
Input: ["Editor","type","type","text","undo","text","undo","text","undo","canUndo","canRedo"]
[[],["hello"],[" world"],[],[],[],[],[],[],[],[]]
Output: [null,5,6,"hello world",true,"hello",true,"",false,false,true]Two typed chunks are two undo steps. Undoing both leaves an empty text, a third undo has nothing to do (false), and the redo history now holds two actions.
Example 2
Input: ["Editor","type","type","type","undo","undo","text","redo","text","redo","text","redo","canRedo"]
[[],["a"],["b"],["c"],[],[],[],[],[],[],[],[],[]]
Output: [null,1,1,1,true,true,"a",true,"ab",true,"abc",false,false]Undo twice leaves "a", redo brings back "ab" then "abc", and a third redo has nothing left to replay.
Example 3
Input: ["Editor","type","delete","text","undo","text","redo","text"]
[[],["hello"],[2],[],[],[],[],[]]
Output: [null,5,2,"hel",true,"hello",true,"hel"]delete(2) reports the 2 characters it removed. Undo restores exactly those characters, redo removes them again.
Each test creates the class with the first operation, then calls the methods in order. Output lists what each call returns; null marks the constructor and methods that return nothing, and their return values aren't checked.
class Editor {
constructor() {
}
/** Append `text`. @return {number} how many characters were typed (0 for an empty string, which is not recorded) */
type(text) {
}
/** Remove up to `count` characters from the end. @return {number} how many were removed (0 means nothing happened and nothing is recorded) */
delete(count) {
}
/** Reverse the latest recorded action. @return {boolean} true if something was undone */
undo() {
}
/** Re-apply the latest undone action. @return {boolean} true if something was redone */
redo() {
}
/** @return {string} the current text */
text() {
}
/** @return {boolean} whether undo() would do something */
canUndo() {
}
/** @return {boolean} whether redo() would do something */
canRedo() {
}
/** Open a group: everything until the matching endGroup() becomes one undo step. Groups nest. */
beginGroup() {
}
/** Close the innermost open group. @return {boolean} false if no group is open */
endGroup() {
}
}
Ctrl/⌘ + ' runs the examples, Ctrl/⌘ + Enter submits. Use console.log freely: output shows under Console. In the editor, press Esc then Tab to move focus out.