Architecture¶
The shape of it¶
Neovim bugsaur (one process) adapter program
┌────────┐ IPC ┌─────────────────────────────┐ DAP ┌─────────┐ ┌─────────┐
│ Lua │◄───────►│ nvim-ipc → debugger-core │◄──────►│ dlv │──►│ your │
│ source │ Unix │ ↕ │ TCP / │ codelldb│ │ code │
│frontend│ socket │ debugger-ui │ stdio │ debugpy │ │ │
└────────┘ └─────────────────────────────┘ └─────────┘ └─────────┘
Everything inside the middle box is one process. The window is not a separate program, and the editor is not in the debug path at all.
Who owns what¶
| Owner | Owns |
|---|---|
debugger-core |
session state — the only authority on what is true right now |
dap-client |
the connection to the adapter and in-flight requests |
| the adapter | the debuggee, and all language-specific knowledge |
| the editor | the breakpoint set of a file, and the source view |
debugger-ui |
how state is displayed, and nothing about what it is |
Two rules follow, and most of the codebase's structure with them:
Nothing but debugger-core decides state. The UI renders a projection; the
editor sends intentions. Neither mutates the model directly.
Nothing but the adapter interprets the language. Bugsaur never parses a value, never guesses a type, and never rewrites a request body. Where adapter specifics are unavoidable, they live in the adapter catalog as data, not as branches in shared code.
The command / event loop¶
debugger-core is a reducer, not a controller:
- Commands are intentions: continue, step, evaluate, expand a variable. They come from the UI or the editor.
- Events are facts: the adapter stopped, a thread appeared, a request answered.
- Effects are what should happen next: send a DAP request, tell the frontend to open a file, raise the window.
The reduction is pure, which is what makes the state model testable without an
adapter, an editor or a window — see crates/debugger-core/tests/.
Stop generations¶
Every stop gets a generation number, and it is the mechanism that keeps stale data off the screen.
When the program moves, the generation advances. Responses that arrive afterwards — a variable page requested before the step, a scope that took too long — are discarded rather than displayed. A value from the previous stop shown next to the current line would be worse than no value at all.
The same generation bounds the variable cache, and it is why expanding a tree is cheap within one stop and starts fresh after the next.
Session phases¶
building exists because Bugsaur compiles some programs itself, before the
adapter starts — see Languages → Go for the measurements
that motivated it. A failed build never reaches the adapter.
The window is a view¶
debugger-ui is built on egui and holds no authority. It renders a projection
of the state — a flattened, virtualised list of visible rows — and turns input
into commands.
Two consequences visible to users:
- panels dragged with the mouse are not persisted: the layout is rebuilt from configuration on every launch;
- breakpoints cannot be removed in the window, because their owner is the editor.
Where to read more¶
| Topic | Page |
|---|---|
| The protocol layer | DAP model |
| The editor boundary | IPC |
| Why each decision | ADRs |