Build Undo/RedoStep 4 of 5

A New Command, For Free

Step 4: A New Command, For Free

The payoff of the Command pattern: the Editor already does undo and redo for any command, without knowing what it is. Add a Delete command and it slots straight in. The editor code does not change at all (the open/closed principle in action).

  • Delete(pos, length): execute(doc) removes length characters at pos, remembering them so undo(doc) can splice them back in
Hints
Starter code
class Document:
    def __init__(self, text=""):
        self.text = text


class Command:
    def execute(self, doc):
        raise NotImplementedError

    def undo(self, doc):
        raise NotImplementedError


class Insert(Command):
    def __init__(self, pos, text):
        self.pos = pos
        self.text = text

    def execute(self, doc):
        doc.text = doc.text[:self.pos] + self.text + doc.text[self.pos:]

    def undo(self, doc):
        doc.text = doc.text[:self.pos] + doc.text[self.pos + len(self.text):]


class Delete(Command):
    def __init__(self, pos, length):
        self.pos = pos
        self.length = length
        self._removed = ""          # filled in on execute, used by undo

    def execute(self, doc):
        """Remove length chars at pos, remembering them for undo."""
        # TODO:
        #   self._removed = doc.text[self.pos:self.pos + self.length]
        #   doc.text = doc.text[:self.pos] + doc.text[self.pos + self.length:]
        pass

    def undo(self, doc):
        """Splice the removed characters back in."""
        # TODO: doc.text = doc.text[:self.pos] + self._removed + doc.text[self.pos:]
        pass


class Editor:
    def __init__(self, text=""):
        self.doc = Document(text)
        self._undo_stack = []
        self._redo_stack = []

    def do(self, command):
        command.execute(self.doc)
        self._undo_stack.append(command)
        self._redo_stack.clear()

    def undo(self):
        if self._undo_stack:
            command = self._undo_stack.pop()
            command.undo(self.doc)
            self._redo_stack.append(command)

    def redo(self):
        if self._redo_stack:
            command = self._redo_stack.pop()
            command.execute(self.doc)
            self._undo_stack.append(command)

    @property
    def text(self):
        return self.doc.text
Write it on a laptop

The editor and the test runner need a wide screen. Open this page on a desktop browser and your progress will be waiting.