Build Undo/RedoStep 5 of 5

Batching with a Macro

Step 5: Batching with a Macro

A single user action. Like replace all, is really many edits, but it should undo as one step. The Composite pattern solves this: a Macro is a command made of other commands, and it is a command itself, so the editor treats it exactly like any other.

  • Macro(commands): execute(doc) runs each sub-command in order; undo(doc) undoes them in reverse order (so overlapping edits unwind correctly)
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 = ""

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

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


class Macro(Command):
    def __init__(self, commands):
        self.commands = commands

    def execute(self, doc):
        """Run every sub-command in order."""
        # TODO: for c in self.commands: c.execute(doc)
        pass

    def undo(self, doc):
        """Undo every sub-command in reverse order."""
        # TODO: for c in reversed(self.commands): c.undo(doc)
        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.