Design a text editor with undo/redo
Company: Notion
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
Quick Answer: This question evaluates understanding of stateful data structures, operation/command semantics for reversible actions, undo/redo stack management, interface design, complexity analysis, and test coverage for mutable editors.
Part 1: Reversible End Operations
Constraints
- 0 <= len(initial) <= 100000
- 0 <= len(operations) <= 10000
- inserted text contains no ':' character
- delete k is a non-negative integer
- delete(k) removes min(k, current_length) characters
Examples
Input: ('abc', ['insert:de'])
Expected Output: ['abcde', 'abc', 'delete:2']
Explanation: Appending two characters is undone by deleting two characters.
Input: ('hello', ['delete:2'])
Expected Output: ['hel', 'hello', 'insert:lo']
Explanation: The deleted suffix 'lo' must be remembered so it can be inserted back.
Hints
- The inverse of inserting n characters is deleting n characters.
- Deleting is lossy: you must capture the exact removed suffix at apply time.
Part 2: Linear Undo and Redo Bookkeeping
Constraints
- 0 <= len(commands) <= 10000
- inserted text contains no ':' character
- delete k is a non-negative integer
- undo and redo on empty stacks are safe no-ops
- Only effective edits are recorded; empty inserts and deletes that remove zero characters do not clear redo
Examples
Input: ['insert:abc', 'insert:de', 'undo', 'redo']
Expected Output: ['abc', 'abcde', 'abc', 'abcde']
Explanation: The second insertion is undone, then redone.
Input: ['undo', 'redo']
Expected Output: ['', '']
Explanation: Undo and redo on empty stacks do nothing.
Hints
- Undo and redo are last-in-first-out, so stacks match the required access pattern.
- Store enough information in each recorded delete to redo and undo it exactly.
Part 3: Edge-Case Audit for Undo and Redo
Constraints
- 0 <= len(commands) <= 10000
- inserted text contains no ':' or '|' character
- delete k is a non-negative integer
- A delete from an empty document removes zero characters and is not recorded
- An ineffective edit must not clear redo
Examples
Input: ['insert:abcd', 'delete:1', 'delete:1', 'undo', 'undo', 'redo']
Expected Output: ['abcd|1|0', 'abc|2|0', 'ab|3|0', 'abc|2|1', 'abcd|1|2', 'abc|2|1']
Explanation: Consecutive deletes are independently undoable.
Input: ['delete:5', 'undo', 'redo']
Expected Output: ['|0|0', '|0|0', '|0|0']
Explanation: Deleting from an empty document is ineffective and does not create history.
Hints
- Track stack sizes after every command, not just the final document.
- Try placing a zero-delete between an undo and a redo; the redo should still be available.
Part 4: Complexity Ledger for an Immutable Text Editor
Constraints
- 0 <= len(commands) <= 100000
- inserted text contains no ':' character
- delete k is a non-negative integer
- Effective insert of p characters from length n costs n + p
- Effective delete from length n costs n and records the removed length; get costs current length
- Undo/redo of an append of p costs n + p; undo/redo of a truncate of p costs n - p under this model
Examples
Input: ['insert:abc', 'get', 'delete:1', 'undo', 'redo']
Expected Output: [[3, 3, 3], [3, 3, 3], [3, 2, 4], [3, 3, 4], [2, 2, 4]]
Explanation: The ledger tracks immutable-string edit costs and retained history payload.
Input: ['delete:5', 'undo', 'redo', 'insert:']
Expected Output: [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]
Explanation: All commands are ineffective or empty-stack no-ops.
Hints
- You do not need the actual text for this ledger; lengths and operation payload sizes are enough.
- Keep separate payload totals for undo and redo so clearing redo is O(1).
Part 5: Step-by-Step Unit Test Runner for a Text Editor
Constraints
- 0 <= number of suites <= 1000
- 0 <= total number of steps <= 100000
- inserted text contains no ':' character
- Each suite starts with a fresh empty editor
- Assertions are checked immediately after every command
Examples
Input: [[['insert:abc', 'abc'], ['undo', ''], ['redo', 'abc']]]
Expected Output: [-1]
Explanation: A simple undo/redo flow passes.
Input: [[['insert:abcd', 'abcd'], ['delete:1', 'abc'], ['delete:1', 'ab'], ['undo', 'abc'], ['redo', 'ab']], [['insert:x', 'x'], ['redo', '']]]
Expected Output: [-1, 1]
Explanation: The first suite passes; the second fails because redo with nothing to redo should leave 'x' unchanged.
Hints
- Reset the editor state, undo stack, and redo stack for each suite.
- A no-op edit between undo and redo should not clear redo; include that behavior in the reference runner.