Skip to content

Commit 79f2d8d

Browse files
committed
docs(nook): plan split-panes host wiring as four green slices
splitlayout has shipped the geometry foundation but the host still renders one editor viewport keyed off bufman's single active buffer. Wiring it up touches the central editing path, so this decomposes the work into complete, separately-shippable slices and pins the two hard calls up front: one distinct buffer per pane in v1, and pane focus that drives bufman.Switch so the focused pane is the active buffer and every existing handler keeps working unchanged.
1 parent aa56cf4 commit 79f2d8d

2 files changed

Lines changed: 144 additions & 1 deletion

File tree

cmd/nook/ROADMAP.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ Roughly in priority order.
6969
line positions for rendering, and `PaneAt` hit-testing for mouse focus,
7070
all constant-time and unit-tested. What remains is host wiring: rendering
7171
each pane's editor view into its rect, drawing the dividers, routing
72-
input to the focused pane, and the focus and split keybindings.
72+
input to the focused pane, and the focus and split keybindings. The
73+
wiring is decomposed into four green-shippable slices in
74+
`docs/nook/design/01-split-panes-host-wiring.md`.
7375
2. **Full multi-cursor.** Add-cursor-at-next-match (ctrl+d),
7476
select-all-occurrences (alt+d), stack-above/below (ctrl+↑/↓),
7577
split-selection-into-lines (alt+i), multi-line edit at every cursor,
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# Split panes: host wiring plan
2+
3+
Status: design. The geometry foundation (`cmd/nook/internal/splitlayout`)
4+
is built and unit-tested but not referenced by the host. This note plans
5+
the wiring so the feature lands as a sequence of complete, green slices
6+
instead of one multi-hour change that risks half-landing in the central
7+
editor path.
8+
9+
## Where the host is today
10+
11+
The host renders exactly one editor viewport.
12+
13+
- `model.View()` assembles the body as `tree + renderMainColumn() + right`,
14+
joined horizontally (`main.go`, the `pieces` assembly).
15+
- `renderMainColumn()` is `m.bufs.Active().View()` — the single active
16+
buffer, full editor rect.
17+
- `editorSize()` computes that one rect (tree width on the left, right
18+
pane on the right, a 20-column floor).
19+
- Every editing path keys off `m.bufs.Active()` (cursor moves, saves,
20+
completion, LSP requests, find/replace — grep `m.bufs.Active(` shows
21+
~60 call sites).
22+
23+
`bufman.Manager` is a single-active-buffer model. The tab bar cycles the
24+
active buffer; it never shows two buffers at once.
25+
26+
## The central entanglement
27+
28+
Two facts make split panes more than a render change:
29+
30+
1. **One size for all buffers.** `bufman.WithSize(w, h)` loops over every
31+
open buffer and sets them all to the same `w, h`:
32+
33+
```go
34+
func (m *Manager) WithSize(w, h int) {
35+
m.width, m.height = w, h
36+
for i := range m.panes {
37+
m.panes[i] = m.panes[i].WithSize(w, h)
38+
}
39+
}
40+
```
41+
42+
`editor.Pane.View()` takes no arguments; it renders at its stored
43+
size. So two on-screen panes need two different stored sizes, which
44+
the blanket `WithSize` actively fights. The wiring must size each
45+
*bound* buffer to its own pane rect, and the `WindowSizeMsg` handler
46+
must stop calling the blanket setter once a split is live.
47+
48+
2. **Editing routes through the active buffer.** Rewriting all ~60
49+
`m.bufs.Active()` call sites to take a "which pane" argument would be a
50+
huge, error-prone diff. The cheap win: **make pane focus drive
51+
`m.bufs.Switch(idx)`.** The focused pane's buffer *is* the active
52+
buffer, so every existing handler operates on the focused pane with
53+
zero changes. Focus and active-buffer become one concept.
54+
55+
## Hard decisions, pinned now
56+
57+
- **One buffer per pane, distinct, in v1.** Each split pane binds to a
58+
different `bufman` buffer index. Showing the *same* buffer in two panes
59+
with independent scroll/cursor needs detachable viewport state on
60+
`editor.Pane` and is explicitly deferred (it is the harder half and not
61+
required for the headline Zed-parity win of "see two files at once").
62+
- **Focus == active buffer.** Single source of truth. No per-pane cursor
63+
bookkeeping in the host; `bufman` already owns each buffer's cursor.
64+
- **Dividers are free geometry.** `Tree.Rects()` already reserves a
65+
1-cell gap between panes; `Tree.Dividers()` returns the line positions.
66+
Rendering is lipgloss, no new geometry.
67+
- **First-paint rule holds.** `splitlayout.New()` is constant-time and does
68+
no I/O; adding the tree to `newModel` does not touch startup latency.
69+
- **Recursive pump is unaffected.** Split panes add no streaming source.
70+
The search and terminal pumps target the active buffer, which is the
71+
focused pane — they keep working unchanged.
72+
73+
## Slices
74+
75+
Each slice is complete on its own, ships with green tests, leaves no dead
76+
keybinding, and keeps the binary starting instantly.
77+
78+
### Slice 1 — bind the tree, no behavior change
79+
80+
- Add `split *splitlayout.Tree` and `paneBuf map[splitlayout.PaneID]int`
81+
(pane → `bufman` index) to `model`. In `newModel`, `New()` yields one
82+
pane bound to buffer 0.
83+
- Route `renderMainColumn()` and `editorSize()` through
84+
`split.Rects(w, h)`. With one pane the rect equals the full editor area,
85+
so output is byte-identical to today.
86+
- No split keybinding yet, so no dead keys.
87+
- Tests: for a matrix of widths, heights, tree-shown, and right-pane
88+
states, the single-pane rect equals the legacy `editorSize()` result.
89+
90+
This slice is the integration milestone — it reconciles `splitlayout`
91+
geometry with the existing tree/right-pane sizing math, which is the real
92+
risk, while changing nothing the user sees.
93+
94+
### Slice 2 — split, close, render two panes
95+
96+
- `ctrl+w v` (Columns / split right) and `ctrl+w s` (Rows / split down),
97+
matching vim and Zed muscle memory. On split: `SplitFocused`, bind the
98+
new pane to the next open buffer (or the current one if only a single
99+
buffer is open), then `bufs.Switch` to it.
100+
- `renderMainColumn()` iterates `split.Rects()`, sizes each bound buffer
101+
to its rect individually (not `bufman.WithSize`), renders each
102+
`buffer.View()` into its rect, and composes the panes with the lines
103+
from `split.Dividers()`.
104+
- `ctrl+w c` closes the focused pane: `CloseFocused`, drop the binding,
105+
collapse. `CloseFocused` already refuses the last pane.
106+
- `WindowSizeMsg` recomputes rects and sizes every bound buffer.
107+
- Tests: split raises pane count and binds a buffer; close refuses the
108+
last pane; rects partition the area with no overlap; the binding map
109+
stays consistent across split/close cycles.
110+
111+
### Slice 3 — focus routing
112+
113+
- `ctrl+w h/j/k/l``FocusDir`, then `bufs.Switch` to the focused pane's
114+
bound buffer. `ctrl+w w``FocusNext`.
115+
- Active-pane affordance: brighten the focused pane's divider/border so
116+
the user can see which split has the cursor.
117+
- `ctrl+w <` / `ctrl+w >``ResizeFocused` to shift the divider.
118+
- If mouse is wired: click → `PaneAt` → focus.
119+
- Tests: directional focus selects the correct neighbor; focusing a pane
120+
changes `bufs.ActiveIndex()` to that pane's bound buffer.
121+
122+
### Slice 4 — reconciliation polish
123+
124+
- Opening a file (picker, finder, go-to-def, tree) targets the focused
125+
pane's binding rather than always the active tab.
126+
- Closing a *buffer* (not a pane) reconciles `paneBuf` indices so no pane
127+
points at a freed slot.
128+
- Per-empty-pane welcome card when a pane's buffer is closed out from
129+
under it.
130+
131+
## Out of scope (record so it is not rediscovered)
132+
133+
- Same buffer in two panes with independent viewports — needs detachable
134+
viewport state on `editor.Pane`. Revisit only after slices 1–4.
135+
- Collab / liveshare — out of scope for nook entirely (see ROADMAP).
136+
137+
## Order of operations
138+
139+
Land 1 first; it is pure de-risking and unlocks the rest. 2 and 3 are
140+
each a single visible feature. 4 is cleanup that can trail. None of the
141+
four needs the deferred independent-viewport work.

0 commit comments

Comments
 (0)