|
| 1 | +# Contributing to code-preview.nvim |
| 2 | + |
| 3 | +Thanks for helping out! This document covers how the plugin is built and how to run the tests. |
| 4 | + |
| 5 | +Before diving in, two pointers to the canonical sources of truth: |
| 6 | + |
| 7 | +- **[CONTEXT.md](CONTEXT.md)** — the glossary. The vocabulary the codebase is written in (*agent*, *proposal*, *preview*, *integration*, *hook entry*, *core handler*, *change*, …). Prefer these terms over synonyms in commits, issues, tests, and code. |
| 8 | +- **[docs/adr/](docs/adr/)** — Architecture Decision Records. The *why* behind the structure below (in-process core handler, one hook entry per OS, forced review gate, origin-prefixed statuses, …). When this doc and an ADR disagree, the ADR wins — and please update this doc. |
| 9 | + |
| 10 | +--- |
| 11 | + |
| 12 | +## How it works (internals) |
| 13 | + |
| 14 | +An **agent** is an external AI coding CLI (Claude Code, OpenCode, Codex CLI, GitHub Copilot CLI) that proposes file edits and asks for permission before applying them. Each agent fires a hook on every **proposal** (one pre-tool firing — an Edit / Write / MultiEdit / ApplyPatch / Bash). code-preview intercepts that hook, renders a **preview** (the per-file diff you open and review), and tears it down once the agent reports the proposal is done. |
| 15 | + |
| 16 | +The pipeline, end to end: |
| 17 | + |
| 18 | +1. **Hook entry** — `bin/hook-entry.{sh,ps1}`. One generic shim *per OS*, shared by every agent, invoked as `hook-entry <agent> <pre|post>` (`.sh` on Unix, `.ps1` on Windows — see [ADR-0008](docs/adr/0008-one-hook-entry-per-os.md)). It takes the agent's native payload, optionally fast-path-filters noisy tools, performs **socket discovery** to find the running Neovim, and makes a single RPC call into the core handler. |
| 19 | + |
| 20 | +2. **RPC transport** — `bin/nvim-call.{sh,ps1}` (caller) and `lua/code-preview/rpc.lua` (dispatcher). Args are written to a JSON tempfile, then `luaeval` invokes the named module function with the decoded args. The dispatcher is the *only* place user-controlled data crosses the shell→Lua boundary, and it never enters a Lua source string. |
| 21 | + |
| 22 | +3. **Core handler** — `lua/code-preview/pre_tool/init.lua` and `lua/code-preview/post_tool.lua`. The agent-neutral pipeline that runs **in-process** inside the user's Neovim ([ADR-0005](docs/adr/0005-core-handler-runs-in-process.md)). It normalises the proposal, decides whether to show a preview (`visible_only` gating, shell-write detection), computes original/proposed content, and drives the diff. This is where everything that *doesn't* depend on which agent fired lives — including `permissionDecision` emission for Claude Code's [review gate](CONTEXT.md#review-gate). |
| 23 | + |
| 24 | +4. **Preview rendering** — `lua/code-preview/diff.lua`. `show_diff()` / `close_diff()`, plus layout resolution (`tab` / `vsplit` share the side-by-side renderer; `inline` is the unified-diff renderer that's the strategic direction, see [ADR-0003](docs/adr/0003-inline-renderer-as-future-default.md)). |
| 25 | + |
| 26 | +An **integration** is the per-agent adapter: an **installer** (`lua/code-preview/backends/<agent>.lua`) that wires the agent's config files to point at the hook entry, plus — for agents that need in-agent glue — adapter code. Only **OpenCode** needs the latter: a TypeScript plugin under `backends/opencode/` that bridges OpenCode's `tool.execute.before/after` API to the shared hook entry. Claude Code, Copilot CLI, and Codex have no `backends/<agent>/` directory — their installers point the agent's native shell-hook config straight at `bin/hook-entry`. |
| 27 | + |
| 28 | +> **Note:** the directory `backends/` and the env var `CODE_PREVIEW_BACKEND` are historical names for what CONTEXT.md now calls an *agent*. Don't rename them, but say "agent" in new code and docs. |
| 29 | +
|
| 30 | +--- |
| 31 | + |
| 32 | +## Architecture |
| 33 | + |
| 34 | +``` |
| 35 | +lua/code-preview/ |
| 36 | +├── init.lua setup(), config, user commands |
| 37 | +├── diff.lua preview rendering: show_diff(), close_diff(), layouts |
| 38 | +├── rpc.lua RPC dispatcher — the shell→Lua boundary |
| 39 | +├── pidfile.lua per-Neovim pidfile for socket discovery |
| 40 | +├── platform.lua per-OS hook-command construction |
| 41 | +├── changes.lua change-status registry (modified/created/deleted/bash_*) |
| 42 | +├── neo_tree.lua neo-tree integration (indicators, virtual nodes, reveal) |
| 43 | +├── health.lua :checkhealth code-preview |
| 44 | +├── log.lua opt-in debug logging |
| 45 | +├── pre_tool/ in-process core handler (pre-tool side) |
| 46 | +│ ├── init.lua orchestration: normalise proposal → decide preview |
| 47 | +│ ├── normalisers.lua per-agent tool payload → canonical proposal |
| 48 | +│ ├── emitters.lua build the RPC/permission responses |
| 49 | +│ └── shell_detect.lua Tier-1 Bash write detection (redirects, mv, sed -i, …) |
| 50 | +├── post_tool.lua in-process core handler (post-tool side): close previews |
| 51 | +├── apply/ in-process edit transformers (edit / multi_edit / patch) |
| 52 | +└── backends/ per-agent installers (claudecode, opencode, copilot, codex) |
| 53 | +
|
| 54 | +bin/ scripts the agent invokes + headless workers |
| 55 | +├── hook-entry.{sh,ps1} generic per-OS hook entry: hook-entry <agent> <pre|post> |
| 56 | +├── nvim-socket.{sh,ps1} socket discovery (pidfile + per-OS fallbacks) |
| 57 | +├── nvim-call.{sh,ps1} RPC caller (JSON args tempfile → luaeval into dispatcher) |
| 58 | +├── apply-edit.lua headless worker: Edit proposal → proposed content |
| 59 | +├── apply-multi-edit.lua headless worker: MultiEdit |
| 60 | +└── apply-patch.lua headless worker: ApplyPatch (custom patch format) |
| 61 | +
|
| 62 | +backends/ |
| 63 | +└── opencode/ OpenCode TS plugin — the only agent needing in-agent glue |
| 64 | + ├── index.ts tool.execute.before/after → hook-entry |
| 65 | + ├── package.json |
| 66 | + └── tsconfig.json |
| 67 | +``` |
| 68 | + |
| 69 | +A **headless worker** is a short-lived `nvim --headless -l <script>.lua` that transforms data *outside* the user's Neovim — no UI, no access to `M.config` or open buffers. The `bin/apply-*.lua` scripts are the canonical examples; the orchestration around them lives in the in-process core handler ([ADR-0005](docs/adr/0005-core-handler-runs-in-process.md)). |
| 70 | + |
| 71 | +--- |
| 72 | + |
| 73 | +## Testing |
| 74 | + |
| 75 | +Tests use [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) for the core plugin and shell scripts for per-agent integration. CI runs on Ubuntu and macOS. |
| 76 | + |
| 77 | +```bash |
| 78 | +./tests/run.sh # all tests (plugin + backends) |
| 79 | +./tests/run.sh plugin # core plugin tests only (plenary busted) |
| 80 | +./tests/run.sh backends # all per-agent integration tests |
| 81 | +./tests/run.sh backends/claudecode # one agent (claudecode|opencode|copilot|codex) |
| 82 | +``` |
| 83 | + |
| 84 | +**Dependencies:** Neovim >= 0.10, jq, bun (for OpenCode tests). Plenary auto-installs to `deps/` on first run. |
| 85 | + |
| 86 | +> **Dogfooding note:** this repo installs code-preview's own hooks (`.claude/settings.local.json`), so edits made by an agent inside this project trigger live previews. After changing plugin code, restart Neovim before testing — the running instance won't pick up the new code otherwise (see `CLAUDE.md`). |
0 commit comments