Buffers and panes

Buffers, panes, splits, file tree, navigation, theme customization.

On this page

A buffer in lattice is anything you can navigate with motions: a code file, a help view, a file tree. Documents and trees both live in the same registry under one BufferId keyspace, so the same :bn / :bp / :ls / :bd / :b N commands work uniformly across kinds. Panes are the visible viewports the buffers render into; you arrange them with vim-style window splits.

Quick reference

Buffer commands

CommandEffect
:e[dit] FILEOpen FILE (or switch to it if already open).
:e folderDefers to :Tree folder -- editing a directory shows it.
:Tree [path]Open a file-tree buffer rooted at path (default: cwd).
:b[uffer] N(planned) Switch the active pane to buffer #N.
:bn[ext]Cycle to the next listed buffer (any kind).
:bp[rev]Cycle to the previous listed buffer.
:lsList every open buffer as static text; % marks the active one.
:buffers / :bOpen the fuzzy buffer switcher (picker) — type to filter.
:bd[elete][!]Close the active buffer. ! discards unsaved document changes.
<C-6> / <C-7>Walk this pane's buffer history back / forward.
:history pane-buffersOpen this pane's buffer history as a picker.
:TreeCloseClose the active pane's tree (alternative to :bd).

Pane commands (vim's <C-w> family)

<C-w> arms a chord; the second key resolves the action. Both "Ctrl held throughout" (<C-w><C-l>) and "release then press" (<C-w>l) forms work.

ChordEffect
<C-w>sSplit pane horizontally (new pane below).
<C-w>vSplit pane vertically (new pane right).
<C-w>c / <C-w>qClose the active pane.
<C-w>h / <C-w>j / <C-w>k / <C-w>lNavigate to spatial neighbour.
<C-w>w / <C-w><C-w> / <C-w><Tab>Cycle to next pane.
<C-w>W / <C-w><BackTab>Cycle to previous pane.
<C-w>z / <C-w><C-z>Zoom the active pane (toggle).

<C-o> / <C-i> walk the unified position-history ring across buffer boundaries -- so <C-o> from inside a help or tree pane returns to the document spot you came from.

Zooming a pane

A split layout keeps two things in view, and is occasionally in the way -- you want one file full-screen to read a long function, and then you want your layout back. <C-w>z (tmux's prefix z) gives the active pane the whole tab; pressing it again restores the split exactly as it was. :zoom-pane does the same from the command line.

Nothing is destroyed. This is the difference from <C-w>o / :only, which really does close the other panes:

<C-w>z<C-w>o / :only
Other panesHiddenClosed
Second pressRestores the layoutNothing left to restore

Navigation gets you out. <C-w>h / j / k / l while zoomed drop the zoom and move to the pane that is spatially there in the real layout -- you do not have to unzoom first. Splitting while zoomed also unzooms, since the new pane exists to be looked at.

Zoom is per tab. Each tab remembers its own zoom, so switching away and back returns you to a zoomed pane still zoomed.

Resizing chords (<C-w>+, <C-w>-, <C-w>>, <C-w><, <C-w>=) do nothing while zoomed, rather than silently reshaping a layout you cannot see and handing it back changed.

A Z marks the zoomed pane on its modeline and its tab. The tabline marker is the one that matters when you have several tabs open: it is the only way to tell a backgrounded tab is zoomed without switching to it. Control it with:

:set pane.zoom-indicator=both       " default -- modeline and tabline
:set pane.zoom-indicator=modeline
:set pane.zoom-indicator=tabline
:set pane.zoom-indicator=none

Walking a pane's buffer history

Each pane remembers the buffers it has shown, and you walk that trail without leaving the pane:

ChordEffect
<C-6>Back one buffer in this pane's history.
<C-7>Forward one buffer.
:history pane-buffersOpen the trail as a picker (<CR> walks to a stop).

You land back at the cursor position you left, not the top of the file.

Per pane, and splits start fresh. Two panes each keep their own trail. Splitting gives the new pane a trail containing only the buffer it is showing — a split is a new place to work, not a copy of where you have been.

Back/forward like a browser. Walking back then opening a new buffer drops what was ahead of you:

open A → B → C          trail: A B C   (on C)
<C-6>                   trail: A B C   (on B)
<C-6>                   trail: A B C   (on A)
<C-7>                   trail: A B C   (on B)
open D                  trail: A B D   (C is gone)

At either end you get a message rather than wrapping around.

What goes into a trail

EventEffect on the pane's trail
Open a file (:e, picker, file tree)Appended as a new stop.
Switch to an already-open buffer (:b, :bn, <C-^>-style hops)Appended — returning to a buffer is a genuine third stop, not a dedup.
A buffer merely created but never shown hereNot recorded. History tracks visits, not buffers.
:e! (reload)The current stop is repointed, not duplicated — same file, fresh contents.
:bdRemoved from every pane's trail.
Preview in a pickerNot recorded. Scrolling candidates never pollutes history.
Split the paneThe new pane starts with one stop: what it is showing.
Close a paneIts trail is discarded with it.
Help / oil / file-tree buffers shown in the paneRecorded like any other buffer — walking back to the help page you were reading works.
Floating popupsNot recorded; they never take over the pane.

The picker

:history pane-buffers opens a real fuzzy picker over the trail — newest first, with * marking where you currently are:

* second.txt:1
  main.rs:42
  README.md:1

<CR> walks to the chosen stop rather than recording a new visit, so everything ahead of it stays reachable with <C-7> — picking an older entry behaves exactly like pressing <C-6> several times. A pane that has not navigated yet reports that it has no history rather than opening an empty picker.

This is not vim's <C-^>, which toggles between two files — pressing <C-6> twice here goes back two buffers. The pair is a genuine back/forward, which a toggle cannot be half of.

pane.buffer-history-size (default 100) bounds each trail; oldest entries are dropped past it.

Semantics

The buffer registry

Every open buffer (document, file tree) gets a stable BufferId when it's created. The id is monotonic, never reused, and outlives any one pane. Two panes can show the same buffer (vim's :vsplit

  • no :e); each pane carries its own viewport stash (cursor + scroll), so the cursors are independent even though the underlying text is shared.

A buffer always knows its kind:

  • Document -- backed by a file (or [no name] if not yet written). Mutations route through the per-document actor; saving with :w writes to disk; :bd checks the dirty flag unless ! is given.
  • FileTree -- a hierarchical view of a directory. Read-only -- mutating operators echo "buffer is read-only". <CR> on a directory toggles expansion; on a file opens it via :e FILE (which switches to / spawns a Document buffer in the active pane).

Buffer flags

Every entry in the registry carries a small set of flags:

FlagDefaultMeaning
listedtrueWhether the buffer appears in :bn / :bp / :ls.
hiddenfalseReserved for "keep loaded without a window" (vim's 'hidden').

Setting listed = false (vim's :setlocal nobuflisted) makes the buffer skip cycling but still reachable by id (:b N). v1 doesn't yet expose a per-buffer :set interface for these flags -- they're API knobs for plugins / future config.

Multiple file trees

:Tree path adds a new tree buffer rooted at path. If a tree at the same root is already open, the active pane switches to it instead of spawning a duplicate. This means you can have several file trees open simultaneously (one per project root) and cycle between them with :bn like any other buffer.

Pane visuals

Each pane renders its actual buffer content -- there are no decorative borders. When more than one pane is visible:

  • A one-row per-pane status line at the bottom of each pane shows the buffer label + cursor position. The active pane's status line is reverse-videoed; inactive ones are dim.
  • A separator column is drawn between vertically-split panes. Horizontal splits don't get an explicit separator -- the upper pane's status line at its bottom edge is the visual delimiter.
  • Inactive panes keep their syntax highlights (refresh_pane_highlights reparses lazily by document text version). A DIM overlay is layered on top so the active pane stands out without losing color.

Theme customization

Style knobs live in the typed-options registry; :set ui.* writes through to App.theme. Available:

OptionTypeDefaultEffect
ui.dim_inactivebooltrueApply DIM modifier on inactive panes' content.
ui.separatorstringSingle character drawn in the vertical-split separator col.
ui.separator_colorstringdarkgrayForeground color name for the separator.
ui.statusline_active_fgstringdefaultForeground for the active pane's status line.
ui.statusline_inactive_fgstringdarkgrayForeground for inactive panes' status lines.

Color names: the 16 ANSI palette (red, green, lightblue, darkgray, ...) plus default / reset for the terminal default. Hex colors arrive post-1.0.

:describe-option ui.dim_inactive opens the spec inline. :options lists every registered option and its current value.

External file changes (autoread)

When a file changes on disk out from under the editor (a git checkout, a code formatter, another editor), Lattice refreshes the buffer — vim's autoread, on by default:

  • No unsaved edits → the buffer silently reloads to the new on-disk content, cursor and scroll preserved, with a "file" reloaded message.
  • Unsaved edits that conflict → Lattice never clobbers your work. It opens a diff resolver and you reconcile hunk by hunk — see Autoread conflicts in the diff guide for the full walkthrough. In short: transfer the hunks you want with do (:diffget), finalize with :diff-accept, then :e! your buffer to load the resolved file. While the resolver is open the buffer is left alone.
  • File deleted on disk → the buffer is kept (never wiped); you get a warning.

The reload lands on its own — you do not have to press a key to bring it in. Changes to buffers you are not currently in are remembered and applied when you switch to them, which is vim's checktime-on-BufEnter behaviour.

That last point is worth knowing when driving git from magit: a b b checkout rewrites working-tree files while the magit buffer is the one you are in, so a file open in another split keeps showing its old contents until you focus it. Focusing it reloads it. Nothing is lost either way — a file with unsaved edits goes through the diff resolver rather than being overwritten.

Turn it off globally with :set noautoread, or per buffer with :setlocal noautoread. Detection is a lightweight filesystem watch on the directories of your open files only — a huge project with a few files open costs a few watches and no idle work, so project size never affects performance. Your own :w never triggers a spurious reload.

Edge cases

  • Closing the last buffer. :bd rejects when the registry has one entry; the App always has at least one buffer.
  • External change during unsaved edits. See External file changes — the diff resolver opens; local edits are never overwritten.
  • Closing the last pane. <C-w>c rejects when only one pane is open; vim's behaviour is the same.
  • Following a tree entry while the tree is the active pane. The tree buffer stays in the registry; the active pane switches to the file's Document buffer. The user can <C-w>v :Tree path first if they want to keep the tree visible alongside the file.
  • Two panes showing the same file. Each pane has its own cursor / scroll stash. The active pane's hot-path fields (App::cursor, App::scroll) mirror the active pane's stash; switching panes round-trips through the stash.
  • Stale position-history entries. If a buffer is closed via :bd and the position-history ring has entries from it, those entries are filtered out by the <C-o> / <C-i> walker -- reachability is checked against the live registry.

Discovering more

  • :help (no arg) -- open the topic index.
  • :help <topic> -- open a free-form doc. <Tab> lists registered topics. The index page links every topic via [namename`` markdown links so <CR> jumps.
  • :describe-command name -- introspection-driven doc for a registered command. Cross-links to :help <topic> when a topic covers the command.
  • :describe-key chord -- bindings for a chord across modes.
  • :describe-option name -- typed option metadata + current value.
  • :apropos pattern -- substring search across all command metadata.

Anchors in design.md

  • §5.9 -- everything-is-a-buffer principle and the pane / window composition model.
  • §5.6 -- rendering / theming layers.
  • §5.12 -- typed options registry.
  • §5.1.1 -- unified position history (jump list + mark ring).