Skip to content

Commit f7fe235

Browse files
committed
docs: add high-frequency command architecture plan
Signed-off-by: GBLMX <muzhibaoxiang@163.com>
1 parent 92624c8 commit f7fe235

2 files changed

Lines changed: 297 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ key dismisses it.
6868

6969
- [Project charter](PROJECT_CHARTER.md)
7070
- [Known issues](docs/known-issues.md)
71+
- [High-frequency command architecture plan](docs/command-architecture-plan.md)
7172
- [0.3.0 → 0.4.0: Polish & Specification](docs/polish-and-specification.md)
7273
- [0.3.0 → 0.4.0 / Phase 0.7 plan](docs/phase-0-7-plan.md)
7374
- [Plugin-host isolation ADR](docs/adr/0012-plugin-host-isolation.md)

‎docs/command-architecture-plan.md‎

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
# High-Frequency Command Architecture Plan
2+
3+
Status: planning only. No command registry, keymap trie, effect queue, Lua
4+
keymap host, or command capability matrix is implemented by this document.
5+
6+
## Goal
7+
8+
Make Anvil's high-frequency editor actions fast, asynchronous where needed,
9+
and uniformly authorized without putting Lua on the keystroke path.
10+
11+
The first slice is **not** `make`. Build commands are lower-frequency and
12+
higher-risk. The architecture should be proven on commands users invoke many
13+
times per minute:
14+
15+
- `editor.undo`, `editor.redo`
16+
- `search.next`
17+
- `view.split`, `buffer.next`
18+
- `lsp.completion`, `lsp.hover`, `lsp.definition`
19+
- `editor.save`
20+
- `lsp.rename`, `lsp.code_action`, `lsp.format`
21+
22+
## Non-goals
23+
24+
- No Lua callback per key event.
25+
- No Lua direct mutation of the editor buffer.
26+
- No Go gateway round-trip for pure editor commands.
27+
- No `proc.spawn` blanket capability.
28+
- No `fs.write` implied by applying a workspace edit.
29+
- No make/build implementation in the first slice.
30+
- No replacement of the existing text model or selection model.
31+
32+
## Current State
33+
34+
- `crates/anvil-tui/src/commands.rs` dispatches Ex commands with string
35+
matching; it is not a command registry.
36+
- `crates/anvil-tui/src/keys.rs` resolves keys through hardcoded `pending`
37+
state and `match` arms; there is no trie.
38+
- `crates/anvil-tui/src/lsp_ui.rs` calls LSP operations directly; they are not
39+
command IDs or effects.
40+
- `crates/anvil-tui/src/workspace_edit.rs` applies edits to buffers and may read
41+
unopened files, but never writes files.
42+
- `crates/anvil-plugin-host` has no Lua/Luau VM yet.
43+
- `anvil-api` has `editor.register_command` as a method name but no command
44+
descriptor or effect DTO.
45+
- The Go gateway owns `sys.*`, `fs.*`, and `lsp.*`; the session currently
46+
starts with `-allow-read -allow-write` and a trusted, unsandboxed LSP path.
47+
48+
This is a new kernel slice, not a small TUI refactor.
49+
50+
## Architecture
51+
52+
```text
53+
key event
54+
-> Rust keymap trie
55+
-> CommandId
56+
-> CommandDispatcher
57+
-> local capability check
58+
-> native execution OR bounded effect queue
59+
-> editor effects or gateway effects
60+
-> result/event with request id and revision
61+
```
62+
63+
Lua appears only in two places:
64+
65+
1. startup declarations: keymap, aliases, command descriptors;
66+
2. custom command implementations that submit effects.
67+
68+
Lua never appears in the per-key resolution loop.
69+
70+
## 1. CommandRegistry
71+
72+
Place the registry in a future `anvil-session`/`anvil-command` boundary, not
73+
`anvil-core`. `anvil-core::EditCommand` is a text/undo primitive, not a UI
74+
command.
75+
76+
Each command descriptor should include:
77+
78+
```text
79+
CommandId stable namespaced ID
80+
Aliases :w, :wa, etc.
81+
ArgsSchema command-line arguments
82+
ExecutorKind Native | Plugin
83+
Capabilities editor.write, fs.read, fs.write, sys.exec, ...
84+
Contexts select, insert, cmdline, overlay
85+
Priority interactive | background
86+
Concurrency immediate | single-flight | cancellable
87+
Result status, events, error
88+
```
89+
90+
Rules:
91+
92+
- `:w` and `Space` menu entries reference the same canonical command ID.
93+
- Plugin commands are namespaced; they cannot override core command IDs.
94+
- Built-in and plugin commands share the same descriptor shape.
95+
- The registry exposes a bounded metadata query for command palette/menu; the
96+
key path stores an index, not a string lookup.
97+
98+
## 2. Keymap trie
99+
100+
- One trie per input context: select, insert, cmdline, overlay.
101+
- `Space` is a prefix node, not a special branch in `keys.rs`.
102+
- Trie leaves store a command index or `CommandId`.
103+
- Key resolution is allocation-free on the hit path, lock-free, and never
104+
calls Lua or JSON-RPC.
105+
- Trie snapshots are immutable and swapped atomically after config reload.
106+
- Prefix timeouts, counts, and operator-pending state stay in the TUI adapter
107+
or keymap state; they are not command implementations.
108+
- The `Space` menu is generated from the registry/trie, not a second hardcoded
109+
list.
110+
111+
## 3. Lua/Luau configuration
112+
113+
Lua runs outside the keystroke path and may only declare:
114+
115+
```lua
116+
anvil.keymap.set("select", "<Space>f", "picker.open")
117+
anvil.keymap.set("select", "<Space>r", "lsp.rename")
118+
anvil.keymap.set("select", "K", "lsp.hover")
119+
anvil.keymap.set("insert", "<C-Space>", "lsp.completion")
120+
```
121+
122+
A Lua custom command submits effects:
123+
124+
```text
125+
Lua handler
126+
-> ctx:submit_effect(effect)
127+
-> request id / status
128+
```
129+
130+
It cannot set `EditorState` directly. All editor changes go through the
131+
dispatcher and its revision checks.
132+
133+
Startup configuration is atomic: parse, validate, resolve collisions, build
134+
trie, then swap. A bad mapping must not partially replace the previous map.
135+
136+
## 4. Effect queue
137+
138+
Effects are typed values, not arbitrary callbacks:
139+
140+
- `editor.apply_edits`
141+
- `editor.set_selection`
142+
- `ui.notify`
143+
- `lsp.request`
144+
- `fs.read`
145+
- `fs.write`
146+
- `sys.exec`
147+
- `sys.fetch`
148+
149+
Every invocation carries a request id, command id, source/plugin, deadline,
150+
cancellation token, and expected revision where applicable.
151+
152+
Queue rules:
153+
154+
- Pure editor commands execute immediately and do not queue.
155+
- LSP completion/hover are single-flight; a newer request cancels or replaces
156+
the older one.
157+
- Save is single-flight per buffer; a later save request coalesces with the
158+
pending one.
159+
- Bounded queues; explicit overflow behavior; interactive work is not starved
160+
by background work.
161+
- Stale results are dropped by path, request id, and revision.
162+
- Lua command results contain effects; they are authorized before being
163+
applied.
164+
165+
## 5. Authorization
166+
167+
Authorization has two layers:
168+
169+
1. **CommandDispatcher (editor/session)**: checks the command's required
170+
capabilities and context before execution.
171+
2. **Gateway**: checks external effects (`sys.*`, `fs.*`, `lsp.start`) and
172+
applies sandbox/resource policy.
173+
174+
Pure editor commands do not go through the gateway. The gateway is not the
175+
editor command registry.
176+
177+
### Capability matrix
178+
179+
| Command | Required capabilities | Disk write? |
180+
|---|---|---|
181+
| `editor.undo`, `editor.redo` | none | no |
182+
| `search.next`, `selection.*` | none | no |
183+
| `view.split`, `buffer.next` | none | no |
184+
| `lsp.completion`, `lsp.hover` | LSP request | no |
185+
| `lsp.definition` | LSP request, `fs.read` for target open | no |
186+
| `picker.open`, `tree.toggle` | `fs.read` | no |
187+
| `lsp.rename` | LSP request, `editor.write`, `fs.read` for unopened files | **no** |
188+
| `lsp.code_action` | LSP request, `editor.write`, `fs.read` for unopened files | **no** |
189+
| `lsp.format` | LSP request, `editor.write`, `fs.read` for unopened files | **no** |
190+
| `editor.save`, `:wa` | `fs.write` | yes |
191+
192+
`WorkspaceEdit` application updates buffers and creates ghost buffers; it does
193+
not write files. `fs.write` is only for an explicit save command.
194+
195+
## 6. High-frequency migration order
196+
197+
### Phase A: pure synchronous commands
198+
199+
Migrate `editor.undo`, `editor.redo`, `search.next`, `view.split`, and
200+
`buffer.next`.
201+
202+
Acceptance:
203+
204+
- Same editing behavior and undo boundaries.
205+
- No async delay or gateway call.
206+
- Key dispatch benchmark remains in the microsecond range.
207+
- Registry and trie tests cover conflicts and count/prefix handling.
208+
209+
### Phase B: cancellable LSP queries
210+
211+
Migrate `lsp.hover`, `lsp.completion`, and `lsp.definition`.
212+
213+
Acceptance:
214+
215+
- Completion cancels/replaces stale requests.
216+
- Hover has one in-flight request.
217+
- Results carry path and revision; stale results do not mutate state.
218+
- Key latency is independent of language-server latency.
219+
220+
### Phase C: save
221+
222+
Migrate `editor.save` and `:wa`.
223+
224+
Acceptance:
225+
226+
- Single-flight per buffer.
227+
- Explicit `fs.write`.
228+
- Failed save leaves the buffer dirty and reports the error.
229+
- No save writes from workspace-edit application.
230+
231+
### Phase D: mutating LSP commands
232+
233+
Migrate `lsp.rename`, `lsp.code_action`, and `lsp.format`.
234+
235+
Acceptance:
236+
237+
- Apply WorkspaceEdit with `editor.write` and optional `fs.read`.
238+
- No `fs.write` during apply.
239+
- Optional `:wa`/save is a separate authorized effect.
240+
- Overlapping/invalid edits do not partially mutate buffers.
241+
242+
### Phase E: high-frequency pickers
243+
244+
Migrate `picker.open` and `tree.toggle`, then address the known
245+
selection-scroll bug separately.
246+
247+
### Phase F: Lua declarations and plugin commands
248+
249+
Only after the plugin host owns a VM:
250+
251+
- configuration parsing and atomic keymap swap;
252+
- plugin command descriptors and effect submission;
253+
- no per-key Lua callbacks.
254+
255+
### Phase G: build commands
256+
257+
Defer `build.run`/make until a build sandbox and explicit build policy exist.
258+
A generic `proc.spawn` capability is not acceptable.
259+
260+
## 7. Contract requirements
261+
262+
Before implementing, freeze:
263+
264+
- Command ID names and namespaces.
265+
- Effect payload schema and error kinds.
266+
- Capability names, using the existing `sys.exec`, `sys.fetch`, `fs.read`,
267+
`fs.write` vocabulary unless a versioned migration is approved.
268+
- Context names for select/insert/cmdline.
269+
- Revision and cancellation semantics.
270+
- Conflict behavior for duplicate key mappings and duplicate command IDs.
271+
272+
## 8. Verification plan
273+
274+
- Unit tests for registry lookup, duplicate IDs, alias resolution, and
275+
capability metadata.
276+
- Trie tests for prefix collisions, timeouts, counts, and mode switching.
277+
- Dispatcher tests proving pure commands never call Lua or the gateway.
278+
- Effect queue tests for cancellation, single-flight, coalescing, stale results,
279+
and bounded overflow.
280+
- WorkspaceEdit tests proving no disk writes during apply.
281+
- Save tests proving `fs.write` is required and failure preserves dirty state.
282+
- A hot-path benchmark for key resolution.
283+
- Existing TUI behavior tests remain green.
284+
285+
## Open questions
286+
287+
- Command registry ownership: `anvil-session` versus a dedicated
288+
`anvil-command` crate.
289+
- Whether `lsp.*` methods become first-class commands or remain effect
290+
providers behind command IDs.
291+
- Whether context predicates are limited to declarative fields or permit
292+
plugin-defined predicates (the latter risks the hot path).
293+
- Whether the gateway should expose a lightweight local policy-decision API, or
294+
the dispatcher and gateway share a capability table without RPC.
295+
- Exact priority policy for interactive LSP requests versus background index
296+
work.

0 commit comments

Comments
 (0)