Instructions for AI-assisted development. This document defines how we build software together.
- Define contracts before implementations
- Interfaces belong in the consumer package
- Accept interfaces, return concrete types
- Small interfaces (1-3 methods) are preferred
- Every major component should be swappable
- Dependencies are explicit, passed via constructors
- No global state, no hidden dependencies
- No magic — if it's used, it's passed in
- Start with code that has zero internal dependencies
- Build upward: utilities -> domain -> application -> infrastructure -> entry point
- Each file should compile and test before moving to the next
- Business logic: always test
- Complex algorithms: always test
- I/O boundaries: integration test
- Glue code: rarely needs tests
- Offer to write tests when a unit of work is testable
mdview.nvim is a local markdown viewer for Neovim. It has three components:
- Rust server (
server/) — HTTP + WebSocket server that discovers markdown files, renders them, and serves a web UI - Lua plugin (
lua/mdview/) — Neovim integration that manages the server lifecycle and syncs buffer state - Browser frontend (
server/static/) — Single-page app with sidebar navigation, markdown rendering, and dark mode
Neovim (Lua) ──WebSocket──▶ Rust Server ──HTTP──▶ Browser
│ │
│ BufEnter/BufWritePost │ File discovery
│ sends current file path │ Markdown rendering
│ │ Static file serving
└──────────────────────────┘
- Rust for the server — Single binary, fast startup, no runtime dependencies for the end user
- Vanilla frontend — No build step. HTML/CSS/JS served directly.
highlight.jsfor code blocks - WebSocket for sync — Bidirectional: Neovim tells server which file is active, server pushes updates to browser
- Thin Lua layer — Neovim plugin only handles process lifecycle and autocmds. All logic lives in the server
mdview.nvim/
├── README.md
├── CLAUDE.md
├── docs/
│ ├── ARCHITECTURE.md
│ └── TREE.md
├── notes/
│ ├── rust/
│ ├── neovim/
│ └── patterns/
├── server/
│ ├── Cargo.toml
│ ├── src/
│ │ ├── main.rs # Entry point, CLI args, server startup
│ │ ├── discovery.rs # Recursive .md file discovery
│ │ ├── render.rs # Markdown -> HTML rendering
│ │ ├── ws.rs # WebSocket hub (Neovim + browser connections)
│ │ ├── routes.rs # HTTP route handlers
│ │ └── state.rs # Shared application state
│ └── static/
│ ├── index.html # Single-page app shell
│ ├── style.css # Styling + dark mode
│ └── app.js # Sidebar tree, WebSocket client, navigation
├── lua/
│ └── mdview/
│ └── init.lua # Commands, autocmds, server lifecycle
└── plugin/
└── mdview.lua # Auto-registration
| Phase | Files | Goal |
|---|---|---|
| 1 | server/src/discovery.rs |
Walk directory tree, collect .md files |
| 2 | server/src/render.rs |
Markdown to HTML with pulldown-cmark |
| 3 | server/src/state.rs |
Shared state: file tree, active file, connected clients |
| 4 | server/src/routes.rs |
HTTP endpoints: file tree API, rendered content, static files |
| 5 | server/src/ws.rs |
WebSocket hub for Neovim + browser sync |
| 6 | server/src/main.rs |
Wire everything together, CLI args |
| 7 | server/static/* |
Browser UI: sidebar, content pane, dark mode |
| 8 | lua/mdview/init.lua |
Neovim commands, autocmds, process management |
| 9 | plugin/mdview.lua |
Auto-registration on load |
- Confirm scope — Single responsibility, clear purpose
- Identify interfaces — What contracts does it need or provide?
- Implement — Keep it minimal, make it work
- Test — If it's testable and non-trivial, write tests
- Verify — No compile errors, tests pass
- Document — Update notes if something new was learned
- Confirm — Get approval before moving to next file
- Never output multiple files in a single response
- Always confirm before writing the next file
- Exception: multiple files only if explicitly requested
- Ask for relevant existing files or context
- Don't assume implementation details
- Clarify requirements before proceeding
- Use
axumfor HTTP routing - Use
pulldown-cmarkfor markdown rendering - Use
tokio-tungstenitefor WebSocket - Use
notifyfor file watching - Use
clapfor CLI argument parsing - Error handling: use
anyhowfor application errors,thiserrorfor library errors - Async runtime:
tokio
- Use
vim.fn.jobstart()for spawning the server process - Use
vim.fn.system()for one-shot commands - Autocmds:
BufEnter(sync active file),BufWritePost(trigger reload) - Keep the Lua layer as thin as possible — no business logic
- No framework, no build step
- Use
fetch()for API calls - Use native
WebSocketfor live sync - Use CSS custom properties for dark/light theming
- Use
highlight.jsfor code block syntax highlighting (loaded via CDN)
notes/
├── rust/ # Rust-specific: axum patterns, async idioms, error handling
├── neovim/ # Neovim plugin patterns: jobstart, autocmds, commands
└── patterns/ # Design patterns: WebSocket hub, file watching, state sync
Create a note when:
- A new concept is used for the first time
- A non-obvious technique solves a problem
- A gotcha or edge case is discovered
# {{Concept Name}}
## What
[One paragraph explanation]
## Why
[When to use this, what problem it solves]
## Example
[Code demonstrating the concept]
## Gotchas
[Common mistakes, edge cases]
## Related
[[other-note]], [[another-note]]| Priority | What | Why |
|---|---|---|
| High | File discovery logic | Core feature, many edge cases |
| High | Markdown rendering | Must handle malformed input |
| Medium | WebSocket message routing | Integration point |
| Medium | API response shapes | Contract with frontend |
| Low | Server startup/wiring | Glue code |
- Unit tests in the same file (
#[cfg(test)]) - Integration tests in
server/tests/ - Use
tempdirfor file system tests
"I need to see [specific file] to understand [what]."
"Here's the plan for [component]:
- [Step]
- [Step] Does this approach work?"
"Ready to implement [file]. Confirm?"
"This [component] is testable. Want me to write tests?"
Maintain a STATUS.md file at the project root. This file tracks progress across sessions.
- Update at the end of each session with what was accomplished
- Include: what was built, decisions made, what's next
- Keep it concise — a running log, not a report