# How Flow Control's Infinite Undo System Works in the Buffer Implementation

> Discover how Flow Control's infinite undo system works in its buffer implementation. Learn how snapshots in a linked list enable unlimited history traversal.

- Repository: [CJ van den Berg/flow](https://github.com/neurocyte/flow)
- Tags: internals
- Published: 2026-03-08

---

**Flow Control implements an infinite undo system by storing a complete snapshot of the text buffer in a singly-linked list of heap-allocated `UndoNode` objects after every edit, allowing unlimited history traversal until memory exhaustion.**

The open-source editor Flow Control (neurocyte/flow) provides an unbounded undo/redo capability that persists full buffer states rather than change deltas. This article examines the implementation details found in the source code, tracing how the system captures snapshots, manages branching redo paths, and integrates with the editor UI.

## Core Architecture of the Infinite Undo System

The infinite undo mechanism centers on persistent data structures and explicit memory management. Rather than applying inverse transformations (deltas), the system treats each edit as an immutable checkpoint.

### The UndoNode Data Structure

Each point in history is represented by an `UndoNode` struct defined in `src/buffer/Buffer.zig` at lines 81-88. Every node stores:

- The complete `root` of type `Buffer.Root` (the entire file's tree structure)
- A `next` pointer for the singly-linked list
- An optional `meta` byte slice for UI annotations
- The file's EOL mode

Because each node holds a full `Root` tree rather than a diff, restoration requires no computation—only pointer swapping.

### Singly-Linked List Design

The `Buffer` struct maintains two list heads: `undo_head` and `redo_head`. When `store_undo` is called (lines 99-101), the system creates a new node via the allocator at lines 104-112, duplicates any metadata, and pushes it onto `undo_head`. The push/pop helpers at lines 115-118 and 120-124 manage these transitions using standard linked-list operations.

## Recording and Restoring Buffer States

The API surface exposes three primary operations: `store_undo`, `undo`, and `redo`. Each operates in constant time relative to buffer size, though memory consumption grows linearly with edit count.

### Storing Undo Points

Before any mutating operation, the editor calls `store_undo` (lines 99-101). This function:

1. Allocates a new `UndoNode` on the heap using `self.allocator.create`
2. Captures the current `root` state
3. Pushes the node onto the undo stack

There is no capacity check or circular buffer logic—nodes accumulate indefinitely, creating the "infinite" behavior.

### Performing Undo and Redo Operations

The `undo` function (lines 149-158) pops the head from `undo_head`, pushes it onto `redo_head`, and restores the buffer's root to the snapshot stored in the node. Conversely, `redo` (lines 161-174) walks the redo list, moving nodes back to the undo stack and re-applying their stored states.

Both operations preserve the EOL mode and metadata stored in the node, ensuring complete state restoration.

### Handling Redo Branches

Flow Control supports non-linear redo history through the `UndoBranch` mechanism (lines 90-93). When a user undoes several steps and then types new text, the system must preserve the previously available redo path.

The `push_redo_branch` function (lines 136-146) saves the current `redo_head` chain as a branch attached to the current undo node, then clears `redo_head`. This allows users to navigate back to the previous timeline using "undo-redo-undo" sequences, effectively creating a tree of histories rather than a simple stack.

## Editor Integration and History Management

While `Buffer.zig` contains the primitive operations, `src/tui/editor.zig` orchestrates when to record history and how to batch related changes.

### Pausing and Resuming Undo Recording

The editor provides `pause_undo_history` (lines 33-38) and `resume_undo_history` (lines 41-53) to group multiple buffer mutations into a single undo step. During macro replay or complex commands, the UI calls `pause_undo_history` to temporarily disable snapshotting. When resumed, the system captures the delta between the paused state and current state as one consolidated undo node.

### Metadata and UI Feedback

The `store_undo_meta` function allows the editor to attach opaque byte slices to undo nodes. This metadata enables the UI to display descriptive labels like "insert 'hello'" or "delete selection" in the command palette. The editor passes this meta buffer to `store_undo`, which stores it alongside the root snapshot.

## Practical Implementation Example

The following Zig code demonstrates the direct Buffer API for managing undo history:

```zig
const std = @import("std");
const Buffer = @import("src/buffer/Buffer.zig");

// Assume `buf` is a *Buffer initialized with an allocator
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();

// Capture metadata for the operation
var undo_meta = try buf.store_undo_meta(allocator);
defer allocator.free(undo_meta);

// 1. Record an undo point before mutation
try buf.store_undo(undo_meta);

// 2. Perform an edit (e.g., insert text)
try buf.insert(allocator, cursor_pos, "example text");

// 3. Undo the change - restores buffer to previous root
const undone_meta = try buf.undo();
// `buf.root` now points to the state before insertion

// 4. Redo the change - moves forward in history
const redone_meta = try buf.redo();
// Buffer state restored to include "example text"

```

In production usage, the TUI editor wraps these calls in command handlers, automatically invoking `store_undo` before each user-initiated mutation and managing the pause/resume lifecycle for batched operations.

## Summary

- **Complete snapshots**: Flow Control stores full `Buffer.Root` trees in `UndoNode` objects rather than diffs, enabling instant restoration.
- **Unbounded growth**: The singly-linked list in `src/buffer/Buffer.zig` has no size limit; history grows until OOM conditions occur.
- **Branching redo**: The `UndoBranch` mechanism preserves alternative timelines when users undo and then create new edits.
- **Editor coordination**: `src/tui/editor.zig` controls when to pause recording for atomic operations and attaches metadata for UI descriptions.
- **Constant-time operations**: `undo()` and `redo()` operate in O(1) time relative to buffer size, trading memory for speed.

## Frequently Asked Questions

### How does Flow Control handle memory usage with infinite undo?

The system makes no attempt to limit memory consumption. Every call to `store_undo` allocates a new heap node containing the complete buffer state. Memory usage grows linearly with the number of edits until the process encounters an out-of-memory condition, at which point the operating system or allocator handles the failure.

### What is redo branching and how does it work?

Redo branching occurs when you undo several changes, then make a new edit. Normally this would destroy the redo history, but Flow Control saves the current redo chain as a `branch` on the active `UndoNode` (via `push_redo_branch` at lines 136-146). You can later navigate back to this branch through specific undo-redo sequences, maintaining access to the abandoned edit path.

### Can the undo history be paused during batch operations?

Yes. The editor implements `pause_undo_history` and `resume_undo_history` in `src/tui/editor.zig` (lines 33-53). When paused, mutations do not create new undo nodes. Upon resuming, the system captures the net change as a single undo point, effectively treating the entire batch as one atomic operation from the user's perspective.

### Does Flow Control use delta or snapshot-based undo?

Flow Control uses **snapshot-based** undo. Each `UndoNode` stores a complete copy of the `Buffer.Root` tree at the moment of capture. This design choice eliminates the need to compute inverse operations or replay edit sequences, allowing O(1) state restoration at the cost of higher memory usage compared to delta-based systems.