How Caxton works
Caxton's speed at large sizes is not an optimization pass on an ordinary editor. It comes from four decisions made at the start: map the file instead of loading it, keep edits in a piece table, index lines in tiers, and draw only the viewport with Core Text. This page describes each, and what they cost.
The file is memory mapped
Opening a file maps it into the address space read-only. Nothing is copied, converted, or loaded up front, which is why a 50 GB log opens as fast as a 10 KB config. The operating system pages bytes in when something reads them and drops them again under pressure, so the app's memory footprint does not grow with the file.
Mapped files can change underneath an editor: a network volume drops, a disk is ejected, another process truncates the file. The mapping is created so that a page the backing store can no longer supply reads as zeros instead of crashing the app, and every read is clamped to the file's current size, revalidated as the file is watched. The file watcher then delivers the real event: a reload, or a prompt.
Edits live in a piece table
The document is a list of pieces. Each piece points at a run of bytes in one of two buffers: the mapped original, which never changes, or an append-only buffer that holds everything you have typed or pasted. Inserting text appends the new bytes and splices a piece into the list. Deleting text only changes the list. The original file is never modified until you save.
- A keystroke costs the same on a 50 GB document as on a small one, about a millisecond, because it never moves the file's bytes.
- Reading the document at a moment in time is a cheap snapshot: a copy of the piece list with shared buffers. Search, filter, save, and the index all read snapshots, so they run in the background while you keep typing.
- Adjacent pieces that point at contiguous bytes are merged in the background, so a long editing session does not degrade lookups.
Undo records each edit as an operation against the piece table, so undoing a Replace All across more than a million sites is bookkeeping, not copying, and it lands as one step. Undo history is limited by memory rather than by count. The limit is sized from the memory the system could actually give back at that moment, and when an operation is too large to keep, Caxton applies it and says that the step cannot be undone instead of silently dropping history.
The line index is tiered
Line numbers, Go to Line, filtering, and the gutter all need to know where each line starts. The index is built in the background from the first moment the window appears, and everything works while it builds: the editor renders and edits against what has been indexed so far.
- Tier 1 keeps line offsets in memory.
- Tier 2 takes over past 20 million lines: offsets move to a memory-mapped scratch file, 8 bytes per line, so an index for hundreds of millions of lines does not sit in RAM. Scratch files are swept at the next launch if the app was killed.
- Edits do not rebuild the index. Each edit is recorded as a small delta against the base offsets, and deltas are folded into the base in batches.
A request for a line past the indexed frontier jumps to the frontier, says so, and completes on its own when the index arrives. Percent and byte targets need no index at all.
Rendering: an NSView and Core Text, not TextKit
The text view is a plain NSView that draws with Core Text. On each frame it asks the index which lines intersect the viewport, reads those bytes through the piece table, lays them out, and draws them. Laid-out lines are cached against the document's version counter, so an edit invalidates exactly what it must.
Long lines get the same treatment horizontally. Every line is split into fixed-size byte segments, and only the segments that overlap the visible horizontal range are decoded, laid out, and drawn. That is why a 500 MB file that is a single line scrolls, and why you can jump deep into that line and land at once.
TextKit was not an option. It lays text out from a text storage object that holds the document as a string, and a mapped 50 GB file has no such string to hand over. Building one would mean loading the file, which is the thing this design exists to avoid.
What leaving TextKit costs
Everything TextKit gives an app for free had to be written, and is maintained, by hand:
- Hit testing, caret movement by grapheme cluster, selection, multiple cursors, and rectangular selection.
- Input method support through
NSTextInputClient, including marked text for composed input. - Soft wrap, with its own row map, because there is no layout manager to ask.
- Printing, find highlights, change markers, folding, and the minimap.
- Accessibility. The text view reports itself as a text area and posts value and selection changes, which is thinner today than what a TextKit view provides.
The trade is deliberate. The cost is engineering time; the return is that every feature runs at file scale, and that numbers on the benchmarks page hold at 50 GB.
Saving is atomic, and interrupted saves recover
A save streams the piece table to a temporary file beside the original and swaps it in, so a crash or power cut during a save cannot leave a truncated file. Before writing, the save compares the file on disk with what was opened, and again before the swap, so an external change is never overwritten without asking. A file opened through a symbolic link is written to its target.
When the edits allow it, Caxton patches the file in place instead of rewriting it. Before the first byte is written, the original bytes of every region it will touch are recorded in a patch journal and flushed to disk. If the process dies mid-patch, the next launch finds the journal and offers to roll the file back.
Separately, a recovery journal records your unsaved edits every 30 seconds, off the main thread, in Caxton's own storage. It never touches the original file. After a crash, the next launch offers to restore the session's edits, and a journal whose file has moved offers Locate, Discard, or Decide Later rather than being deleted. More in Sessions & Recovery.
The grid is the same document
The CSV grid is not an import. It is a second view of the same piece table. A background pass builds a record index with one entry per CSV record: where the record starts and how many fields it has. Field positions are not stored; the grid parses the handful of records on screen each frame. That keeps the index small at ten million rows.
- A cell edit is a splice into the piece table, the same as typing in the text view, so an edit in either view shows in the other and undo covers both.
- Column edits make one small edit per row and adjust the table in place, instead of rewriting and re-parsing the file.
- Saving writes the same file back. There is no export step, and content you did not touch is not reformatted.
- Sorts run in place up to 256 MB and stream the sorted rows to a new document past that, with the source unchanged.
Everything is local
There is no server component. Files are never uploaded, there is no cloud processing, and there is no telemetry. The only network calls are the update check and license activation, both described in the privacy policy.
The measured results of this design, with the methodology to reproduce them, are on the benchmarks page. The reasons it exists are on the about page.