docs: add cross-machine active-work handoff

Add synchronized agent bootstraps and a volatile active-work ledger so
another machine can distinguish durable project state from open
branches, local checkpoints, machine-only providers, and incomplete
verification. Record githubsucks/main as the canonical development
line, refresh the #124/protocol-v17 handoff, correct the current
keybinding reference for compile mode, and mark the July roadmap as a
historical snapshot.
This commit is contained in:
Levi Neuwirth 2026-07-20 10:51:34 -04:00
parent f8096ff826
commit 968e01ae6a
7 changed files with 665 additions and 36 deletions

33
AGENTS.md Normal file
View File

@ -0,0 +1,33 @@
# pmacs agent instructions
**Start here: read `docs/agent-handoff.md`, then
`docs/active-work.md`, before taking on any work.** The handoff carries
durable project state, working method, substrate invariants, and the
standing backlog. The active-work ledger carries volatile branches,
checkpoints, verification, and exact cross-machine recovery commands.
Keep both updated according to their own update protocols.
Always true, independent of the handoff:
- Rust core + Lua runtime (`builtin/runtime/*.lua`), TUI + GPU
(`pmacs-gpu`) frontends over a versioned semantic protocol
(`pmacs-protocol`). `#![forbid(unsafe_code)]`.
- Workflow: framing doc in `docs/` -> user approval -> branch -> implement
-> full gate suite -> PR -> user review rounds -> user says when to
merge. Never merge unprompted. One feature, one branch, one PR.
- Gates before any PR: `cargo fmt --check`; `cargo clippy --workspace
--all-targets -- -D warnings` (as its own step); `cargo test --lib`;
`cargo test --lib --features crdt`; the touched acceptance suites;
`cargo test --test m4_acceptance -- --skip basedpyright`;
`PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu`; `git diff --check`.
- The checkout may be shared with the user: check `git status` for
foreign uncommitted work before stash, checkout, or branch operations,
and never delete untracked files you did not create.
- `githubsucks/main` is the canonical development line. `origin/main`
is a release mirror and may lag substantially; never base new work on
it without comparing both refs.
- Work is portable only after it is committed and pushed. Uncommitted
worktree changes, untracked files, and `/tmp` dependencies do not
travel to another machine.
- Write commit messages with `git commit -F <file>`. Never use
`git add .`.

View File

@ -1,18 +1,19 @@
# pmacs agent instructions
# pmacs agent instructions
**Start here: read `docs/agent-handoff.md` before taking on any work.**
It carries current project state, the working method, substrate
invariants, and the standing backlog — it is the continuity bridge
between development machines. Keep it updated as part of your work
(update protocol is in the file itself).
**Start here: read `docs/agent-handoff.md`, then
`docs/active-work.md`, before taking on any work.** The handoff carries
durable project state, working method, substrate invariants, and the
standing backlog. The active-work ledger carries volatile branches,
checkpoints, verification, and exact cross-machine recovery commands.
Keep both updated according to their own update protocols.
Always true, independent of the handoff:
- Rust core + Lua runtime (`builtin/runtime/*.lua`), TUI + GPU
(`pmacs-gpu`) frontends over a versioned semantic protocol
(`pmacs-protocol`). `#![forbid(unsafe_code)]`.
- Workflow: framing doc in `docs/` → user approval → branch → implement
→ full gate suite → PR → user's review rounds → user says when to
- Workflow: framing doc in `docs/` -> user approval -> branch -> implement
-> full gate suite -> PR -> user review rounds -> user says when to
merge. Never merge unprompted. One feature, one branch, one PR.
- Gates before any PR: `cargo fmt --check`; `cargo clippy --workspace
--all-targets -- -D warnings` (as its own step); `cargo test --lib`;
@ -20,7 +21,13 @@ Always true, independent of the handoff:
`cargo test --test m4_acceptance -- --skip basedpyright`;
`PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu`; `git diff --check`.
- The checkout may be shared with the user: check `git status` for
foreign uncommitted work before stash/checkout/branch operations, and
never delete untracked files you didn't create.
- Commit messages via `git commit -F <file>`, ending with the Claude
co-author line; PR bodies end with the Claude Code attribution.
foreign uncommitted work before stash, checkout, or branch operations,
and never delete untracked files you did not create.
- `githubsucks/main` is the canonical development line. `origin/main`
is a release mirror and may lag substantially; never base new work on
it without comparing both refs.
- Work is portable only after it is committed and pushed. Uncommitted
worktree changes, untracked files, and `/tmp` dependencies do not
travel to another machine.
- Write commit messages with `git commit -F <file>`. Never use
`git add .`.

169
docs/active-work.md Normal file
View File

@ -0,0 +1,169 @@
# Active work — cross-machine resume ledger
**Snapshot: 2026-07-20.** This file records volatile work that has not
landed on `main`. Read it after `docs/agent-handoff.md`. Remove completed
entries when their PR merges; do not let this become a second permanent
backlog.
## Repository authority
- Canonical development remote: `githubsucks`
(`https://github.com/levineuwirth/pmacs.git`).
- Canonical base at this snapshot:
`githubsucks/main` @ `f8096ff` (#124 merged, protocol v17).
- `origin/main` @ `d3fa632` is the release mirror and was 400 commits
behind `githubsucks/main` at the snapshot. Do not base new work on it.
- The shared desktop checkout contained unrelated uncommitted work. The
branches below were prepared in isolated worktrees; never clean or
overwrite the shared checkout to recover them.
Start on another machine with:
```sh
git fetch githubsucks --prune
git log -1 --oneline githubsucks/main
git worktree list
git status --short --branch
```
The first command must expose `f8096ff` or a newer intentional main.
If it does not, stop and repair the remote/fetch configuration.
## Active lane: PR #123 — JSON + YAML
- PR: <https://github.com/levineuwirth/pmacs/pull/123>
- Public PR branch: `githubsucks/json-yaml-grammar`
- Public PR head before the transfer checkpoint: `4be2a65`
- Original merge base: `56eb67e`; current main is nine commits ahead.
- Portable checkpoint branch:
`githubsucks/json-yaml-handoff-2026-07-20`
- The checkpoint is a continuation branch for recovery, not a second
feature and not a merge target. Finish there, then push its completed
head to `githubsucks/json-yaml-grammar`.
The checkpoint carries the unpushed review-fix set that previously lived
only in `../pmacs-jsonyaml`:
- push configured settings via
`workspace/didChangeConfiguration` immediately after `initialized`;
- fake-LSP configuration sink and a deterministic delivery test;
- explicit `json.validate.enable = true`;
- JSON provider pin
`@t1ckbase/vscode-langservers-extracted@2.0.2`;
- corrected YAML configuration sections:
`yaml`, `http`, `[yaml]`, `editor`, `files`;
- corrected telemetry, schema-network, and schema-association claims;
- PATH-gated real JSON provider integration test.
Verification already completed before transfer:
- JSON provider standalone protocol smoke passed.
- JSON provider through pmacs passed
`m4_real_json_provider_receives_config_and_reports_diagnostics`.
- `m4_json_yaml_lsp_configs_pin_command_and_sections` passed.
- `m4_5_initial_config_pushed_via_did_change_configuration` passed.
- `cargo fmt --check` and `git diff --check` passed.
- `yaml-language-server@1.24.0` standalone protocol smoke passed:
initialization reported version 1.24.0; the initial
`workspace/configuration` request was exactly
`yaml`, `http`, `[yaml]`, `editor`, `files`; opening the document
caused a second scoped `[yaml]` request; invalid YAML produced a real
parser diagnostic; shutdown exited 0 with empty stderr.
Still required on the destination machine:
1. Add the PATH-gated real-YAML pmacs acceptance test, mirroring the
JSON test. Disable SchemaStore and Kubernetes CRD catalog access in
the test for deterministic, network-free operation.
2. Run it against the exact Red Hat provider and require:
auto-attach, initialized state, a non-empty diagnostic, and no crash.
3. Update `docs/json-yaml-framing.md` if the live pmacs path reveals any
difference from the standalone evidence.
4. Rebase the checkpoint onto current `githubsucks/main`.
5. Run the full repository gate suite from `AGENTS.md`.
6. Push the completed head to `githubsucks/json-yaml-grammar`; confirm
new PR checks belong to that head. Never merge without the user's
instruction.
Provider setup is intentionally machine-local:
```sh
npm install --prefix /tmp/pmacs-jsonls \
@t1ckbase/vscode-langservers-extracted@2.0.2
npm install --prefix /tmp/pmacs-yamlls \
yaml-language-server@1.24.0
```
The Node language servers had to run outside the prior machine's
restrictive execution sandbox. The `/tmp` prefixes and smoke harnesses
do not travel.
Recovery worktree:
```sh
git worktree add --track \
-b json-yaml-handoff-2026-07-20 \
../pmacs-jsonyaml-home \
githubsucks/json-yaml-handoff-2026-07-20
```
## Active lane: Arc 4 stage 3 — statusline segments
- Portable branch: `githubsucks/statusline-segments`
- State: framing only, revision 1, based on `f8096ff` / protocol v17.
- Status: awaiting user review. No implementation and no PR.
- Scope: composable per-window Lua modeline providers, dynamic
modeline-face inventory, and protocol v18
`StatuslineSegments`; the built-in LSP segment is the first consumer.
- Do not implement until the user approves the framing. When approved,
continue on this branch so the framing remains the first commit.
Recovery worktree:
```sh
git worktree add --track \
-b statusline-segments \
../pmacs-statusline \
githubsucks/statusline-segments
```
## Parked lane: kill-ring browser + persistence
- Portable branch: `githubsucks/kill-ring-browser`
- State: framing only, revision 2; no implementation and no PR.
- Status: explicitly parked by the user on 2026-07-20.
- Its original scout was based on `0efb5cd`. The preserved framing marks
this ground truth stale and requires a complete re-scout against the
then-current `githubsucks/main` before implementation.
- Compile-mode has merged since the original scout, so old
“compile-mode in flight” keybinding/touch-set assumptions are not
authoritative.
Recovery worktree, only when the user un-parks it:
```sh
git worktree add --track \
-b kill-ring-browser \
../pmacs-kill-ring-browser \
githubsucks/kill-ring-browser
```
## Documentation lane
- Portable branch: `githubsucks/handoff-2026-07-20`
- Carries synchronized `AGENTS.md` / `CLAUDE.md`, this ledger, the
durable handoff refresh, and the keybinding reference correction.
- It changes no runtime code.
- Review and merge this documentation branch separately; it must not be
folded into PR #123 or either feature framing branch.
## Update protocol
Whenever a listed lane changes materially:
1. update its public branch and head/state here;
2. record new verification and remove superseded caveats;
3. keep durable architecture in `docs/agent-handoff.md`, not here;
4. remove the lane after merge or abandonment;
5. verify every recovery command from a clean worktree before calling
the transfer complete.

View File

@ -1,7 +1,8 @@
# Agent handoff — cross-machine continuity
**Last updated: 2026-07-15, after multi-language injections (#122)
merged; also carries the #120 themes-stage-1 snapshot.** This file is the
**Last updated: 2026-07-20, after the cross-machine continuity audit;
main still reflects GPU font preferences (#124), themes stage 1 (#120),
and injections (#122).** This file is the
bridge between development machines. If you are an agent reading
this on a fresh clone: this document plus the `docs/*-framing.md`
files ARE your memory. Read this fully before taking on work, seed
@ -9,11 +10,14 @@ your persistent memory from it, and **update this file (and commit
it) whenever project state changes materially** — the next machine
reads it the way you just did.
## 1. Where the project stands (2026-07-15)
For volatile branches, checkpoints, verification, and recovery
commands, read `docs/active-work.md` immediately after this file.
- `main` @ `5e73966` (multi-language injections #122 merged; #120
themes stage 1 below it), protocol **v16** (`SUPPORTED=[6..16]`;
v15→16 shipped the `ThemeFacts` channel — injections added no wire).
## 1. Where the project stands (2026-07-20)
- `main` @ `f8096ff` (GPU font preferences #124 merged), protocol
**v17** (`SUPPORTED=[6..17]`; v15→16 shipped `ThemeFacts`, v16→17
shipped `FontFacts`).
- **Syntax-highlight / language-detection side-quest (#114#118)
LANDED** — a one-shot arc built in sibling worktrees off main while
the user's themes lane (`theme-faces`) ran concurrently in the shared
@ -114,12 +118,38 @@ reads it the way you just did.
survive both sides) and the **store-sourced diag-count freeze**
(per-URI severity totals in `DiagnosticStore`, O(1), survive
`mark_stale`).
- **NEXT: themes stage 2 — `pmacs.gpu.set_font` at protocol v17**
(shipped versions are never reused; the `pmacs-gpu-design.md:299`
no-wire-change claim is superseded and must be corrected in the
stage-2 framing). Glyphon font reload was flagged HARD. Stage 3
after: Lua statusline-segment API (segments carry face names).
Workflow as always: framing → user approval → branch → gates → PR.
- **Themes (Arc 4) stage 2 LANDED — #124 merged after the complete
behavioral review** (`docs/gpu-set-font-framing.md` rev 5):
`pmacs.gpu.set_font { family?, size? }` is a live global preference;
authoritative bufferless `FontFacts` is gated at protocol v17.
GPU family resolution is frontend-local and fail-closed to a
sanitized monospace default across normal/bold/italic queries;
metrics for all seven glyphon buffers derive atomically from one
logical-pixel size. The visual-run caret substrate normalizes
source bytes through adornments and shaped clusters (including
combining sequences/ligatures), preserves deliberately scrolled-away
viewports, and reflows on font, gutter, minimap, text, CRDT, resize,
and snapshot geometry changes. Test fixtures enter fontdb before
`FontSystem` construction; alternate advances are measured across
complete shaped runs, not sampled glyphs.
- **NEXT: themes stage 3 — Lua statusline-segment API.**
Revision 1 is preserved on branch `statusline-segments` and awaits
review. It scopes additive per-window modeline providers, face-name
segments, dynamic `ThemeFacts` inventory, and a new v18
`StatuslineSegments` channel; the existing LSP status tracker is the
first built-in provider. Approval → implementation → gates → PR.
Completing stage 3 completes Arc 4.
- **OPEN: JSON + YAML PR #123.** The public PR branch still points to
the original two-commit implementation (`4be2a65`). Review fixes and
live-provider evidence are preserved on checkpoint branch
`json-yaml-handoff-2026-07-20`; the real YAML-through-pmacs smoke,
rebase, and full gates remain. Do not mistake the PR's old green CI
for validation of the checkpoint. Exact recovery is in
`docs/active-work.md`.
- **PARKED: kill-ring browser + persistence.** Revision 2 framing is
preserved on branch `kill-ring-browser`, but its `0efb5cd` scout is
stale and must be repeated before implementation. No PR or
implementation is active.
- Roadmap: `docs/roadmap-2026-07.md` (ranked arcs). Position:
- **Arc 1 (LSP utility surface) COMPLETE** — completion popup
(#92/#93), panels/references/outline/hover (#94#96), plus
@ -238,8 +268,9 @@ buffer owns a path's recovery slot; only recover/discard release
unclaimed crash data; adopt clears the old owner's skip cache.
**Protocol** — encoding-breaking bumps are deliberate and versioned
(`SUPPORTED=[6..15]`). v15 = `CompletionPopup` + `StatusFacts.message`.
New wire surface ⇒ bump + both-frontends support + acceptance.
(`SUPPORTED=[6..17]`). v15 = `CompletionPopup` +
`StatusFacts.message`; v16 = `ThemeFacts`; v17 = `FontFacts`. New wire
surface ⇒ bump + both-frontends support + acceptance.
**Fake LSP** (`src/bin/pmacs_fake_lsp.rs`) modes: `fullonly`,
`rangeonly`, `rangeonly16` (UTF-16 + fail-closed bounds validation),
@ -323,7 +354,9 @@ runtime/Lua-registered languages (v1 resolves only against
`BUILTIN_LANGUAGES`), and the next injection *consumers* gated on new
grammars — HTML/CSS/GraphQL/SQL (`<script>`/`<style>`, JS/TS template
literals, doc-comment code); modeline detection as a 5th layer
(`-*- mode: … -*-` / `# vim: ft=…`); JSON/YAML grammars+LSP;
(`-*- mode: … -*-` / `# vim: ft=…`); JSON/YAML grammars+LSP
(PR #123 open; checkpoint and remaining verification are in
`docs/active-work.md`);
byte-accurate multibyte cursor placement in `move_active_cursor_to`
(still steps one codepoint per LSP byte column). A full Jupyter `.ipynb`
setup (reader → editable → kernel execution) is a real arc now gated on
@ -358,5 +391,7 @@ Don't expect them in a clone; on the desktop, never delete them.
When a PR merges, an arc opens/closes, or a decision lands: edit the
snapshot (§1), append lessons (§5) and deferrals (§6) as they arise,
bump the date line at the top, and commit — usually riding the same PR
as the work. Keep it under ~250 lines: this is a briefing, not a log;
prune sections that stop being true.
as the work. Keep durable architecture here; put branch hashes,
machine-local tools, incomplete verification, and recovery commands in
`docs/active-work.md`. This is a briefing, not a log: prune sections
that stop being true.

379
docs/keybindings.md Normal file
View File

@ -0,0 +1,379 @@
# pmacs keybindings — reference
**Last verified against `main` @ `f8096ff` (2026-07-20).** This is a
snapshot, not generated output — when a PR adds, removes, or rebinds a
key, update this file in the same PR (see §6). If you're an agent and
this file looks stale against the code it cites, trust the code.
pmacs keys come from two independent places:
- **The Lua keymap** (§12) — `pmacs.keymap.bind{...}` calls, resolved
by the Rust dispatcher against whatever `init.lua` has bound at
runtime. Fully user-rebindable: unbind or rebind any of these from
init.lua (§5).
- **Rust-hardcoded modal shadows** (§3) — isearch, query-replace,
the minibuffer/prompt, the completion popup, and the context menu
each shadow the Lua keymap while active: `EditorInstance::dispatch_key`
(`src/editor.rs:658-733`) checks these modes, highest-priority first,
before a key ever reaches the Lua dispatcher. **Not user-configurable**
— there is no `pmacs.keymap` surface for them; changing one means
editing the mode's `from_chord` decoder in Rust.
Notation matches what `pmacs.keymap.bind` accepts: `C-` = Ctrl, `M-` =
Alt/Meta, `S-` = Shift, bare letters/punctuation self-insert when
unmodified. Named keys are angle-bracketed (`<left>`, `<up>`, `<home>`)
or all-caps (`RET`, `BS`/Backspace, `DEL`/Delete, `TAB`, `SPC`).
Sequences separated by spaces (`C-x C-s`) are chords typed in order.
## 1. Global keymap
Source: `builtin/keymaps/default.lua` unless noted. All bound at
`scope = "global"`.
### Cursor motion
| Key | Command |
|---|---|
| `C-a` / `<home>` | `cursor.line-start` |
| `C-e` / `<end>` | `cursor.line-end` |
| `C-f` / `<right>` | `cursor.right` |
| `C-b` / `<left>` | `cursor.left` |
| `C-n` / `<down>` | `cursor.down` |
| `C-p` / `<up>` | `cursor.up` |
| `C-<left>` / `M-b` | `cursor.word-left` |
| `C-<right>` / `M-f` | `cursor.word-right` |
| `C-<up>` / `M-{` | `cursor.paragraph-up` |
| `C-<down>` / `M-}` | `cursor.paragraph-down` |
| `<pageup>` / `M-v` | `cursor.page-up` |
| `<pagedown>` / `C-v` | `cursor.page-down` |
| `M-g g` / `M-g M-g` | `cursor.goto-line` (`builtin/runtime/editops.lua`) |
### Selection (CUA shift-select)
Plain motion preserves an existing selection instead of dropping it
(Emacs-flavored default, not strict CUA).
| Key | Command |
|---|---|
| `S-<left>` / `S-<right>` | `cursor.select-left` / `cursor.select-right` |
| `S-<up>` / `S-<down>` | `cursor.select-up` / `cursor.select-down` |
| `S-<home>` / `S-<end>` | `cursor.select-line-start` / `cursor.select-line-end` |
| `C-S-<left>` / `C-S-<right>` | `cursor.select-word-left` / `cursor.select-word-right` |
| `C-S-<up>` / `C-S-<down>` | `cursor.select-paragraph-up` / `cursor.select-paragraph-down` |
### Editing
| Key | Command |
|---|---|
| `BS` | `buffer.delete-backward` |
| `DEL` / `C-d` | `buffer.delete-forward` |
| `RET` | `edit.newline-and-indent` |
| `TAB` | `buffer.tab` |
| `C-BS` / `C-h` | `buffer.delete-word-backward` (see §4 for the `C-h` rationale) |
| `M-BS` | `buffer.delete-word-backward` |
| `C-DEL` | `buffer.delete-word-forward` |
| `M-d` | `buffer.delete-word-forward` |
| `M-u` | `edit.upcase` (`editops.lua`) |
| `M-l` | `edit.downcase` (`editops.lua`) |
| `M-c` | `edit.capitalize` (`editops.lua`) |
| `C-t` | `edit.transpose-chars` (`editops.lua`) |
| `M-t` | `edit.transpose-words` (`editops.lua`) |
| `M-z` | `edit.zap-to-char` (`editops.lua`) |
| `M-<up>` / `M-<down>` | `edit.move-line-up` / `edit.move-line-down` (`editops.lua`) |
| `M-^` | `edit.join-line` (`editops.lua`) |
| `M-;` | `edit.toggle-comment` (`builtin/runtime/comment.lua`) |
> `M-d` / `M-BS` currently plain-delete the word — they are **not**
> kill-ring members yet (a named deferral; see `docs/agent-handoff.md`
> §6, "word kills"). `edit.kill-line` (below) is the only word/line
> kill wired into the ring so far.
### Clipboard & kill ring
| Key | Command |
|---|---|
| `M-w` | `edit.copy` |
| `C-w` | `edit.cut` |
| `C-y` | `edit.paste` |
| `C-x h` | `edit.select-all` (Emacs `mark-whole-buffer`) |
| `C-k` | `edit.kill-line` (`builtin/runtime/killring.lua`) |
| `M-y` | `edit.yank-pop` — replace the just-yanked text with the previous kill, immediately after `C-y` (`killring.lua`) |
### Undo / redo
Multiple bindings exist because terminals disagree on how `Ctrl+/`
encodes; see §4.
| Key | Command |
|---|---|
| `C-/` / `C-_` / `C-4` / `C-x u` | `buffer.undo` |
| `C-?` / `C-S-_` / `C-x r` | `buffer.redo` |
### Search & replace
Once a search is running, `C-s`/`C-r` step to the next/previous match
and `M-r` toggles literal↔regex — those are Rust-hardcoded isearch
keys, not Lua bindings (§3).
| Key | Command |
|---|---|
| `C-s` | `search.forward` (starts isearch) |
| `C-r` | `search.backward` (starts isearch) |
| `C-M-s` | `search.forward-regex` |
| `C-M-r` | `search.backward-regex` |
| `M-%` | `query-replace` (starts an interactive replace session, §3) |
| `C-M-%` | `query-replace-regexp` |
### Multi-key (`C-x`) chords
| Key | Command |
|---|---|
| `C-x C-s` | `buffer.save` |
| `C-x C-c` | `editor.quit` |
| `C-x 2` | `window.split-horizontal` |
| `C-x 3` | `window.split-vertical` |
| `C-x o` / `C-x O` | `window.focus-next` / `window.focus-prev` |
| `C-x 0` | `window.close` |
| `C-x 1` | `window.close-others` |
| `C-x b` | `editor.switch-buffer` |
| `C-x C-b` | `editor.list-buffers` (opens the `*buffer-list*` panel, §2) |
| `C-x <right>` / `C-x <left>` | `editor.next-buffer` / `editor.previous-buffer` |
| `C-x C-r` | `recent-files` (`builtin/runtime/recentf.lua`) |
### Command palette & cancellation
| Key | Command |
|---|---|
| `M-x` | `editor.execute-command` — prompts (via the minibuffer, §3) for any command by name |
| `C-g` | `editor.cancel` — resets the dispatcher / clears an unfinished prefix |
### Completion
| Key | Command |
|---|---|
| `C-M-i` | `completion.at-point` (`builtin/runtime/completion.lua`) — opens the popup; popup navigation is Rust-hardcoded (§3) |
### LSP
Source: `builtin/runtime/lsp.lua`. `M-.` follows the cross-editor
go-to-definition convention; the rest sit on the `C-c` prefix to keep
printable letters free for self-insert.
| Key | Command |
|---|---|
| `M-.` | `lsp.go-to-definition` |
| `M-?` | `lsp.find-references` (opens `*references*` panel, §2) |
| `M-,` | `lsp.jump-back` (unwind the cross-file jump ring) |
| `C-c o` | `lsp.document-symbols` (opens `*outline*` panel, §2) |
| `C-c r` | `lsp.rename` |
| `C-c a` | `lsp.code-actions` |
| `C-c i` | `lsp.inlay-hints` |
| `C-c y` | `lsp.semantic-tokens` |
| `C-c h` | `lsp.hover` |
| `C-c H` | `lsp.hover-doc` (opens `*lsp-help*` panel, §2) |
| `C-c s` | `lsp.signature-help` |
| `C-c f` | `lsp.format-buffer` |
`builtin/runtime/lsp.lua` initially binds `M-g n` / `M-g p` to
diagnostic navigation. `compile.lua` loads afterward and deliberately
replaces them with the unified error dispatcher below.
### Compile, shell command, and unified errors
Source: `builtin/runtime/compile.lua`.
| Key | Command |
|---|---|
| `M-g n` / `M-g p` | `error.next` / `error.previous` — compile/grep errors when that source has claimed navigation, otherwise LSP diagnostics |
| `` C-x ` `` | `error.next` |
| `M-!` | `shell.command` — asynchronous output in `*shell-command*` |
`compile.run` and `compile.recompile` are available through `M-x`; no
global key is assigned to them.
## 2. Buffer-local panel keymaps
Read-only panel buffers built on `pmacs.listview.open` (buffer scope
`{ scope = "buffer", buffer = <id> }`; see `builtin/runtime/listview.lua`)
all share one keymap:
| Key | Action |
|---|---|
| `RET` / `SPC` | `listview.visit` — act on the item under the cursor |
| `n` / `<down>` | `cursor.down` |
| `p` / `<up>` | `cursor.up` |
| `g` | `listview.refresh` — re-run the data source and re-render |
| `q` | `listview.quit` — restore the buffer that was active before the panel opened |
Panels currently built on this: `*references*`, `*outline*`,
`*lsp-help*` (hover docs). Header text always spells out the same
`RET`/`n`/`p`/`g`/`q` legend inline.
`*buffer-list*` (`editor.list-buffers`, `C-x C-b`) uses its own
keymap, layered on the same idiom, in `builtin/commands/default.lua`:
| Key | Command |
|---|---|
| `RET` / `SPC` | `editor.buffer-list-visit` |
| `n` / `<down>` | `cursor.down` |
| `p` / `<up>` | `cursor.up` |
| `d` | `editor.buffer-list-mark-delete` |
| `u` | `editor.buffer-list-unmark` |
| `x` | `editor.buffer-list-execute` — kill every marked buffer |
| `k` | `editor.buffer-list-kill-now` |
| `g` | `editor.buffer-list-refresh` |
| `q` | `editor.buffer-list-quit` |
One-off buffer-local bindings, each scoped to a single generated
buffer:
| Buffer | Key | Command |
|---|---|---|
| `*workers*` (`editor.list-workers`) | `C-c C-k` | `workers.cancel-at-point` (`builtin/runtime/async.lua`) |
| `*pmacs-instance*` (`editor.describe-instance-buffer`) | `q` | `buffer.kill-this` (`commands/default.lua`) |
| `*help*` (`editor.describe-command`) | `q` | `buffer.kill-this` |
| REPL buffers (`builtin/packages/repl/init.lua`) | `RET` | `pmacs.repl.submit-current` |
| REPL buffers | `C-c` | `pmacs.repl.send-sigint-current` |
| REPL buffers | `C-d` | `pmacs.repl.send-eof-current` — closes stdin on an empty line, else deletes forward |
Compile-mode generated buffers (`*compilation*` and
`*shell-command*`) have their own buffer-local map:
| Key | Command |
|---|---|
| `RET` | `compile.visit-error` |
| `n` / `p` | `compile.next-error-line` / `compile.previous-error-line` |
| `q` | `compile.quit` |
| `C-c C-k` | `compile.kill` |
| `g` | `compile.recompile` (`*compilation*` only) |
| every shipped undo/redo chord | `compile.undo-noop` — generated output is intercept-read-only |
The REPL package (`builtin/packages/repl/`) is shipped but opt-in —
loaded via `require`, not part of the always-on `builtin/runtime`
lane. Its bindings only exist in a buffer created by a REPL session.
## 3. Rust-hardcoded modal keys
These live in `src/editor.rs` (and `src/minibuffer.rs` for the
prompt) as small `from_chord(chord) -> Action` decoders, one per mode,
checked in priority order by `EditorInstance::dispatch_key`
(`src/editor.rs:658-733`, highest first): **context menu → isearch →
query-replace → minibuffer → completion popup → normal Lua dispatch.**
Each decoder's rustdoc names its own key list; this table mirrors
those. They are not reachable through `pmacs.keymap` — there is
deliberately no Lua surface for them (keeps the set curated; see the
`R51` rationale cited in `lib.rs`/`lua_bindings/mod.rs`).
**Isearch** (`SearchKey`, `editor.rs:1858-1919`) — active after
`C-s`/`C-r`/`C-M-s`/`C-M-r`:
| Key | Action |
|---|---|
| `C-s` / `<down>` | next match |
| `C-r` / `<up>` | previous match |
| `RET` / `C-m` | accept — keep cursor + highlights |
| `C-g` / `Esc` | cancel — restore the origin cursor |
| `BS` / `C-h` | shorten the query by one character |
| `M-r` | toggle literal ↔ regex |
| any printable char | extend the query |
**Query-replace** (`QueryReplaceKey`, `editor.rs:1926-1961`) — active
after `M-%`/`C-M-%`:
| Key | Action |
|---|---|
| `y` / `SPC` | replace this match, advance |
| `n` / `BS` / `Delete` | skip this match, advance |
| `!` | replace this and every remaining match, no more prompts |
| `.` | replace this match, then quit |
| `q` / `RET` / `Esc` / `C-g` | quit (replacements already made are kept) |
**Minibuffer / prompt** (`MinibufferAction`, `minibuffer.rs:418-527`)
— backs every `pmacs.minibuffer.read` call: `M-x`, query-replace's
from/to prompts, `find-file`, etc.:
| Key | Action |
|---|---|
| `RET` / `C-m` | accept |
| `C-g` | cancel |
| `TAB` / `C-i` | complete to the selected candidate |
| `<up>` / `<down>` | prev/next candidate if a dropdown is showing, else history navigation |
| `C-p` / `C-n` | history prev/next, unconditionally |
| `<left>` / `C-b`, `<right>` / `C-f` | cursor move |
| `<home>` / `C-a`, `<end>` / `C-e` | line start/end |
| `BS` | delete backward |
| `Delete` / `C-d` | delete forward |
| `M-n` / `M-p` | scroll the selected candidate forward/back |
| any other printable char | self-insert |
**In-buffer completion popup** (`CompletionPopupKey`,
`editor.rs:2019-2059`) — active after `C-M-i` or an LSP-triggered
popup. Unlike the others this is a **partial** shadow: only the keys
below are intercepted; everything else (typing, motion) falls through
to normal dispatch, so typing keeps self-inserting while the popup is
open.
| Key | Action |
|---|---|
| `<down>` / `C-n` | next candidate |
| `<up>` / `C-p` | previous candidate |
| `TAB` / `RET` | accept the highlighted candidate |
| `Esc` / `C-g` | dismiss |
**Context menu** (`MenuKey`, `editor.rs:1966-2009`) — opened by
right-click, not a keybinding itself, but shadows the keymap while
open:
| Key | Action |
|---|---|
| `<down>` / `C-n` | next item |
| `<up>` / `C-p` | previous item |
| `RET` | invoke the highlighted item |
| `Esc` / `C-g` | cancel |
| any other key | dismiss (click-away semantics) |
**Frontend detach** — `F12` (any modifiers) detaches an attached
frontend from the daemon (`src/attach.rs:997-1006`, checked at
`attach.rs:818`). Not a UI mode inside the editor core, but another
literal-`KeyCode` interception outside the Lua keymap; tentative for
v0.1 per the comment there (chosen because F12 is rarely bound to
anything else).
## 4. Terminal-compatibility caveats
- **`C-h` doubles as `C-BS`.** Most terminals without the kitty
keyboard protocol can't disambiguate `Ctrl+Backspace` from
`Ctrl+H` — both legacy paths send byte `0x08`. `C-h` is bound to
`buffer.delete-word-backward` alongside `C-BS` so the shortcut works
on legacy terminals. pmacs does not use `C-h` as an Emacs-style help
prefix; a user who wants that can rebind it.
- **Undo/redo have redundant bindings** (`C-/`, `C-_`, `C-4` for undo;
`C-?`, `C-S-_` for redo) because terminals encode `Ctrl+/` several
different ways. Kitty's keyboard protocol routes most cleanly
through `C-/`; the alternates keep legacy/remote terminals working.
- Kitty-protocol-only chords (e.g. distinguishing `C-i` from `TAB`)
degrade gracefully where noted above — check the frontend's terminal
capability negotiation if a chord seems to not fire.
## 5. Changing bindings
`pmacs.keymap.bind` / `pmacs.keymap.unbind` are ordinary Lua API,
callable from `init.lua`:
```lua
pmacs.keymap.bind { scope = "global", sequence = "C-c g", command = "cursor.goto-line" }
pmacs.keymap.unbind { scope = "global", sequence = "M-z" }
```
`scope = "buffer"` additionally takes `buffer = <id>`; buffer-local
bindings are pruned automatically when that buffer is removed. This
covers §1 and §2 only — §3's Rust-hardcoded modal keys have no Lua
surface (see §3's intro).
## 6. Keeping this file honest
Update this file in the same PR whenever a binding is added, removed,
or moved — same discipline as `docs/agent-handoff.md`. To re-derive it
from scratch instead of trusting the table: grep `builtin/` for
`pmacs.keymap.bind`/`.bind(` and `pmacs.listview.open`, and grep
`src/editor.rs` / `src/minibuffer.rs` for `from_chord`.

View File

@ -4,6 +4,10 @@ Date: 2026-07-07. Produced from a five-way codebase/docs/memory sweep
(core editing + persistence, LSP surface, GPU parity, extensibility +
terminal, deferred-work inventory across all framing docs).
> **Historical planning snapshot.** Several arcs below have since
> landed. Use `docs/agent-handoff.md` for durable current state and
> `docs/active-work.md` for open branches and recovery instructions.
**Decision (2026-07-07): push Arc 1 (LSP utility surface), with Arc 2
(editing table stakes) items interleaved between sub-arcs.**

View File

@ -42,9 +42,11 @@ The direct continuation of the #114#118 grammar/detection stack.
(`<script>`/`<style>`, template literals, doc-comment code).
- **Modeline detection** — a 5th detection layer (`-*- mode: … -*-`,
`# vim: ft=…`) after extension → filetype → filename → shebang.
- **JSON + YAML** grammars + LSP (`tree-sitter-json` +
vscode-json-languageserver; yaml-language-server). JSON is also the
prerequisite for the notebook path.
- **JSON + YAML — PR #123 open.** Grammars and LSP configs exist on the
feature line; review fixes are preserved on
`json-yaml-handoff-2026-07-20`. A real YAML-through-pmacs smoke,
rebase, and full gates remain before review resumes. JSON is also the
prerequisite for the notebook path; see `docs/active-work.md`.
- **More grammars for languages with neither grammar nor LSP** — ruby,
php, html, css, sql, etc.
- **Grapheme / combining-mark awareness** in the text view
@ -211,8 +213,8 @@ domain excluded — see below), `editor.rs` split (7 k lines),
## Excluded — themes / faces main quest (seen, routed elsewhere)
`pmacs.gpu.set_font` + the statusline-segment API; background/selection
theming; per-peer stable presence colors; exact quad colors per
The statusline-segment API (Arc 4 stage 3; framing awaiting review);
background/selection theming; per-peer stable presence colors; exact quad colors per
decoration kind; GPU gutter background layer / wash recolor / chrome
bold-italic-underline; current-line highlight refinements; multi-server
semantic-token *style* blending; the compile-mode **severity→color**
@ -229,8 +231,8 @@ guides (visual, not color).
north-star items is done, so the highest-leverage board is now:
1. **Config registry** — frees ~5 editing/indent/comment features at once.
2. **JSON (+ YAML) grammar** — the one remaining gate on the Jupyter
`.ipynb` path now that injections exist, and a clean highlight one-shot.
2. **Finish JSON + YAML PR #123** — the one remaining gate on the
Jupyter `.ipynb` path now that injections exist.
3. **Locals-query processing** — restores `.builtin` styling for
non-shadowed builtins, the last rough edge of the highlight stack.