1---2title: "Extending 012"3sidebar_position: 34---56# Extending 01278012 grows along two axes: **breadth** (more functions, commands, formats,9charts, overlays, remote sources) and **size** (more cells, sheets and10formulas). Each axis has a few patterns. New work follows them; code that11doesn't yet is listed under [Gaps](#gaps).1213## Breadth: one entry, every surface1415### 1. Registries are the single source1617A capability is one entry in a table, in one file, and every surface that18shows it is derived from the table rather than listing it again.1920| Registry | Entry | Derived from it |21|---|---|---|22| `functions.FuncDef` (one file per category in `internal/functions`) | a function: name, signature, description, arity, eval, result format, decimal twin | parsing, autocomplete, argument hints, help, [Functions](../reference/functions.md) |23| `ui.command` | an action: id, title, description, run, enabled, checked, what it edits, and how macros treat it | key bindings (Sheets and vim), menu bar, context menus, palette, shortcuts help, the `:` command line and its completions, macro recording and `run()` in scripts |24| `fileio` formats (`formats.go`) | a format: name, extensions, labels, traits, importer, exporter | `Import`, `Export`, detection, import picker, File > Download, command line |25| `chart.types`, with `sheet.ChartTypes` | a type: name and order (sheet, saved in files), a layout drawing text and image and the series its legend lists (chart) | `chart.Draw`, `chart.Image`, chart editor, Insert > Chart |26| theme roles (`theme.Theme`) | a role: dark and light styles on the 16 ANSI colors, with a contrast minimum for schemes (`minContrast`) | every style in the UI, drawn in the terminal's palette or any color scheme (`FromPalette`); `TestEveryThemeReadable` checks each role under every built-in scheme |27| `config.Options` | an option: name, type, default, environment variables, flag, live or not, description, check | parsing and warnings, flags, `012 config` and its default file, [Configuration](../reference/config.md), Reload config |28| `locale.table` | a locale: tag, name, Windows LCID, separators, date order and patterns, currency and its place; its language's month and day names (`locale.names`) | parsing typed entries, display, formula separators, CSV, File > Settings > Locale, the `locale` option's values |2930Adding a function, command, format, chart type, option or role means31adding an entry (and its file), not editing switch statements elsewhere.32A function goes in the category file of `internal/functions` it belongs33to (a new file is a new section of functions.md: add it to the34generator's `categories`), reads its arguments with the shared helpers35(`numArg`, `matrixArg`, `each`, `criteriaArgs`, `arrayArg` for arrays),36says in `arrays` whether it takes arrays itself or may return one37(otherwise array contexts map it over arrays for free), and `go test38./internal/functions -run FunctionsDoc -update-docs` regenerates the39reference.40A new process-wide setting is a `config.Options` entry read with41`Config.String`, `Bool`, `Int` or `Duration`; one that belongs to a42workbook goes in the file instead (pattern 9). A new role goes in43`theme.Theme` with its ANSI colors in `New`; schemes pick it up. When a list has to44be kept in sync by hand, turn it into a registry.4546### 2. Components own their state4748UI features that take over input (menus, palette, find bar, pickers, chart49editor, sheet tabs, import flow) are components: a type with its own state,50key and mouse handling, layout and status line, owned by `Model` and driven51through `overlay.Overlay` or its siblings. `Model` routes messages and52composes the screen; it doesn't hold a component's fields. Overlays are53composited over the finished grid, so a new one never shifts the layout.5455A component is built with a host: the small interface of what it needs56of the model (reading the sheet or the selection, running a command,57opening or closing an overlay, a note, the theme), which the model58implements, so it can be tested against a fake. When the host stays59small, about ten methods or fewer, the component lives in a package of60its own under `internal/ui` (`picker`, `cmdline`, `findbar`, `sortbar`,61`filterpick`, `suggest`), declares `Host`, and the model implements it62through the `host` adapter in `hosts.go`; parts that need no host at all63take plain values (`tabstrip`, `transfer`). A component that needs more,64or that acts through the model's own machinery, keeps an unexported host65inside package `ui` (`menuHost`, `pivotHost`, `chartHost`, `promptHost`)66rather than exporting half of `Model`; the table in67[Architecture](architecture.md#the-ui) says which and why. Either way,68running a command goes through `runCommand` (pattern 3).6970### 3. Actions go through commands7172Every user action is a registered command, whatever reaches it (key, menu,73mouse, palette). This keeps discoverability automatic, and `runCommand`,74the one place commands run, is where the macro recorder listens: a command75that changes the workbook is recorded by id (`run("format.bold")`), with76the answer to the question it asks. Each command says how macros treat it77(`command.macro`): recorded (the default), a view command recorded as the78selection it leaves (Go to, Select all, Next sheet), or never recorded nor79run from scripts (files, menus, help, undo, macros).8081The rest of the command log is what isn't a command but matters to a82replay: entries as they're accepted, pastes of text, the fill handle,83column borders and dragged tabs, each recorded where it happens with84`Model.record`, and the selection, recorded as a state (where it is just85before something acts on it) rather than as the keys or clicks that moved86it. So movement keys, typing and mouse selection needn't become commands87to be recorded, and a replay doesn't depend on the window's size. A88dialog opened by a command records what was chosen in it once it acts,89as that command answered with the choices (`recordDialog`), and gives the90command an `answer` hook that makes the same choices, so scripts can run91it; its package writes and reads the answer. A new action that changes92the workbook outside a command records itself the same way; anything93unrecorded that changes the workbook while recording becomes a comment in94the script, so a gap shows.9596### 4. Side effects stay at the edges9798`internal/sheet` and `internal/functions` do no I/O, network or terminal99work. Anything outside the process is a small interface the engine asks100(`RemoteSource`, set per workbook with `SetRemote`; functions reach it101through `Book.Ask`) and the UI fulfils asynchronously with `tea.Cmd`,102answering from a cache. Tests substitute fakes (`jev.Client`,103the fake TypeSafe server, `demos/fakejev`).104New remote function families (other models, web lookups, databases) plug in105the same way.106107## Size: narrow paths for hot data108109### 5. One mutation path110111Every change to a workbook goes through `Batch(Change{...}, fn)` and the112history `step`: snapshot what changes, apply, mark dirty, recalculate. Undo,113telemetry, recalculation and the file's dirty flag hang off that one path,114and the rooms of `012 serve`, which order everyone's steps and tell each115participant what the others changed ([Architecture](architecture.md#shared-workbooks)).116New mutations use it; they never write cells around it. A change made over117several calls, as a macro run makes one call per message, opens its step118with `Workbook.Begin` and closes it when done, so it undoes as one; inside119it, `Settle` recalculates what changed so far, so what reads formulas120(scripts, sorting) sees current values. The macro list itself is changed121through the same steps, so saving a macro is undoable and marks the file122modified.123124### 6. Storage behind a small API125126Code reads and writes cells through a narrow API (`cellStore` in127`internal/sheet/store.go`: get, set, delete, count, iterate everything or128a range, a column being a range), never the representation underneath:129column blocks of 16-byte slots holding plain cells' values, with text,130formats and whole formula cells in side tables (see131[Bounds of support](limits.md#sheet-size)). `get` returns a `*Cell`:132a formula's own, which recalculation updates, or for a plain cell one133made for the caller, which it may keep but whose changes reach nothing;134every change goes through `set`. Making that `Cell` costs an allocation135or three, so code that visits many cells asks for only what it needs:136`value` and `peek` for values, `has` and `filledAt` for presence,137`look` for formatting, `richAt` and `richCells` for formulas and notes,138and the key iterators (`keys`, `keysIn`, `anyKeysIn`) for addresses. The139stored occupancy index (`occupancy.go`) holds the blocks; a second says140which rows hold contents, so a range yields its cells in row order at141the cost of what it holds, and the used range, data edges and the next142filled cell are found without scanning.143144### 7. Read ranges as ranges145146Functions read cells through their `Reader` (`internal/functions/reader.go`):147single cells through `cell`, and ranges through `cells`, which yields only148the cells a range holds, so aggregates (`SUM(A:A)`, running totals) cost149their data, and within a recalculation SUM-like functions share running150aggregates per range (`Book.RangeAgg`, the engine's `rangememo.go`) or have151the engine add a range up as it reads it (`Book.Fold`).152Functions that need positions (`MATCH`, `SUMIF`, `INDEX`) take a `matrix`,153which knows the range's full size and which cells hold something; they154walk those (`stored`) and account for the blanks between at once.155156The `Reader` is concrete, and the engine's side of it, the `Book`, takes157and returns only values: a callback or pointer handed through an158interface escapes to the heap, so it would allocate on every range read.159Ranges come through `Book.Scan` in chunks, into buffers the `Reader`160reuses, and a function's own callbacks stay on the stack; aggregates pass161their `Agg` by value to `Book.Fold`. A new way to162read cells follows suit: a `Book` method of values, and the loop over163what it returns in the `Reader`.164165### 8. Work proportional to what changed or what's visible166167Recalculation touches the cells an edit affects (interval trees of range168users by column, dependency indexes by sheet), rendering touches only the169visible cells, and status statistics are cached against a version170counter. A feature that scans the whole sheet per keystroke or per frame171needs a cache or an index.172173The grid is a million rows by 16,384 columns, so no loop may walk the174addresses of a selection, a range or a line: walk the cells the store175holds (`cellsIn`, `inRange`, the occupancy indexes), and keep formats of176whole columns and rows on the line (`lines.go`). `TestCommandsCostTheDataNotTheGrid`177(`internal/ui/vast_test.go`) holds every registered command to this: it178runs each with the whole sheet, a whole column and a whole row selected179and fails past 250 ms or 8 MB, so a new command that walks its selection180fails it.181182### 9. Additive, versioned files183184The `.012` format only gains optional fields, and the version rises only when185a file uses a feature older builds can't read (names, freeze and filters in186v3, several sheets in v4, pivot tables in v5, tables in v6, linked187sources in v7). Old files always load. Settings that older builds can188safely ignore (decimal arithmetic) don't raise the version.189190## Keeping it honest191192### 10. Measure at the seams193194Every seam above has a telemetry span and a `-tags stress` benchmark195(`make stress`, `make stress-report`), and [Bounds of support](limits.md) records196the bounds. A change to a seam comes with its numbers, measured against197the code it replaces.198199Spans nest without a `context.Context` through the engine: start a UI200span through the model's trace (`m.spans.Start`), so it holds what it201causes; hand work on another goroutine `m.spans.Parent()` (in the202`context.Context` if the API already takes one); and give a workbook a203new owner's trace with `SetTrace`. Never keep a "current span" in a204global: `012 serve` runs many programs in one process (see205[Observability](observability.md#nested-spans)).206207### 11. Shape limits208209`make lint`: no function over cognitive complexity 25, no Go file over 500210lines. A package that keeps growing past a few thousand lines gets split211along a boundary where dependencies point one way, as the function212library (`internal/functions`) and values (`internal/value`) sit below213the engine: the lower package defines the small interface it needs214(`functions.Book`) and the engine implements it, keeping its API through215aliases so callers don't change.216217### 12. Every feature at three levels218219Unit tests against the engine or model, an e2e test through libghostty, and220a golden screen reviewed in the gallery for anything visible (see221[Testing](testing.md)).222223## Gaps224225Where the code doesn't follow the patterns yet:226227- The cell entry (typing into a cell, pointing at references) is the228 model's own modes rather than a component with a host (pattern 2); see229 [Architecture](architecture.md#the-ui) for why.230- Movement keys, typing, F4 in formulas, Alt+letter menus and direct mouse231 manipulation act without a registered command (pattern 3). The macro232 recorder covers what matters for replay without them (entries, the233 selection, pastes, the fill handle, column borders, dragged tabs and234 charts, the choices made in dialogs), but changes made in the named235 ranges picker aren't recorded: a recording notes them as comments.