Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: test

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
neovim: [stable, nightly]
steps:
- uses: actions/checkout@v4

- uses: rhysd/action-setup-vim@v1
with:
neovim: true
version: ${{ matrix.neovim }}

- name: Install Lua 5.1 (for luac)
run: sudo apt-get update && sudo apt-get install -y lua5.1

- name: Run tests
run: ./scripts/test.sh
153 changes: 153 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# CLAUDE.md — Maintainer guide for toolpath.nvim

## What this is

Neovim plugin that records editing history as Toolpath Path documents. Thin Lua
wrapper (~500 lines) around `path track` CLI subcommands. The plugin owns undo
tree mapping and VCS awareness; the CLI owns diffing, session state, and
document building.

## Project layout

```
lua/toolpath/init.lua ← all plugin logic (setup, tracking, git, async queue)
lua/toolpath/health.lua ← :checkhealth toolpath
plugin/toolpath.lua ← user command definitions
doc/toolpath.txt ← Neovim :help documentation
README.md ← user-facing docs
local/ ← design docs from the toolpath CLI team (not shipped)
docs/ ← design docs we authored (not shipped)
```

## Version

`M.version` in `lua/toolpath/init.lua` is the single source of truth. Bump it
on every user-visible change. It's displayed in `:ToolpathStatus` and
`:checkhealth toolpath`.

## Checklist for any change

1. **Update `lua/toolpath/init.lua`** — the code itself.
2. **Update `doc/toolpath.txt`** — if you added/changed/removed any command,
Lua API function, config option, buffer variable, or behavior. The help file
is the authoritative reference. Keep the contents table section numbers in
sync.
3. **Update `README.md`** — if the change is user-facing. The README mirrors
the help doc but is less exhaustive. Update the commands table, Lua API
listing, config examples, and any relevant prose sections.
4. **Update `plugin/toolpath.lua`** — if you added/changed/removed a user
command.
5. **Update `lua/toolpath/health.lua`** — if you added new dependencies,
config requirements, or internal accessors.
6. **Bump `M.version`** — in `lua/toolpath/init.lua`.
7. **Run `./scripts/test.sh`** — syntax checks all Lua files and runs the
test suites. Add tests for any new pure functions or API changes.

## CLI interface

The plugin shells out to `path track <subcommand>`. The current subcommands
and their args are documented in `local/track-source-annotate.md` and
`docs/cli-graph-output.md`. Key points:

- `init` — creates session, returns session path on stdout. Accepts
`--base-uri` and `--base-ref` for VCS anchoring.
- `step` — records an edit. Accepts `--source` (JSON vcsSource). Async in
normal flow, sync during gap replay.
- `visit` — caches content for undo/redo navigation. No `--source`.
- `note` — sets intent on head step. Still works, not deprecated.
- `annotate` — generalized note. Can target any step, set intent, source, refs.
- `close` — exports Path document on stdout and deletes session file.
- `export` — like close but non-destructive.

All CLI communication is via args + stdin (buffer content) + stdout (results).
No temp files, no sockets.

## Architecture decisions

**Plugin owns VCS awareness.** The CLI is VCS-agnostic. The plugin runs
`git rev-parse` and passes structured data via `--base-uri`, `--base-ref`,
and `--source` JSON. This keeps the CLI editor-agnostic and VCS-agnostic.

**Git cache refreshed on events, not keystrokes.** `FocusGained` and
`ShellCmdPost` trigger cache refresh. Never run git commands in the
`on_text_changed` hot path.

**Async for step/visit, sync for everything else.** The `on_text_changed`
handler enqueues step and visit calls on a per-buffer serial queue using
`vim.system()` callbacks. Gap replay (intermediate undo entries) is sync
because it moves the buffer through undo states. `init`, `close`, `note`,
`annotate`, and `export` are sync.

**Stop order matters.** `M.stop()` must: (1) delete the augroup first to
prevent new events, (2) drain the async queue synchronously, (3) then call
`close`. Getting this wrong causes races or lost steps.

**Archive only if edits were made.** `stop()` checks `step_count > 0` before
writing to `archive/`. Viewing a file without editing it shouldn't produce
archive files.

## Directory layout for session data

`record.dir` (e.g. `~/.local/share/toolpath`) gets this structure:

```
<dir>/
├── README.md ← auto-generated on first use, explains the directory
├── live/ ← active session state files (temporary, CLI manages lifecycle)
└── archive/ ← exported Path documents (<filename>.<timestamp>.path.json)
```

## Common pitfalls

- **`unpack` vs `table.unpack`**: Neovim's LuaJIT has `unpack` as a global.
Standard Lua 5.2+ uses `table.unpack`. We use `unpack` throughout.
- **Buffer-local autocmds + global events**: `VimLeavePre` is global but our
autocmd uses `buffer = bufnr`. It fires for the active buffer on quit; other
buffers get `BufUnload`. Combined with `once = true`, each buffer's stop
handler fires exactly once.
- **`vim.b` variable cleanup**: Always nil out buffer variables in
`clear_buf_vars()` when stopping. Stale `vim.b.toolpath_tracking = true` on
an untracked buffer would confuse statusline consumers.
- **`vim.validate` positional form**: We use the Neovim 0.10+ positional form
`vim.validate("name", value, "type", optional)`. The older table form is
deprecated.

## Testing

Run all tests locally:

```
./scripts/test.sh
```

This runs `luac -p` syntax checks on all Lua files, then two test suites via
`nvim --headless -l`:

- **`test/test_internals.lua`** — pure function tests for `format_base_uri`,
`build_parent_map`, and `find_step_ancestor` (exported via `M._internals`).
- **`test/test_api.lua`** — public API surface tests (version format,
`_setup_done` state, `statusline()`, `_get_bin()`, config validation).

The test harness is `test/harness.lua` (~50 lines, no external dependencies).

**Adding a new test:** open the appropriate `test/test_*.lua` file and add a
`T.test("name", function() ... end)` block. Use `T.eq(a, b)` for deep equality.

**CI:** GitHub Actions (`.github/workflows/test.yml`) runs the test script on
every push to `main` and every PR, against Neovim stable and nightly.

### Manual verification (not covered by automated tests)

1. `:checkhealth toolpath` — all checks pass
2. Open a file — `vim.b.toolpath_tracking` should be `true` (with auto mode)
3. Make edits — `:ToolpathStatus` shows step count incrementing, no UI lag
4. `:ToolpathStop` — clean shutdown, no errors
5. Check `archive/` — file appears with correct name pattern

## Coordination with the toolpath CLI

Design documents in `local/` come from the CLI team. When the CLI adds new
flags or changes behavior, update `local/` with their docs and adapt the
plugin accordingly. The `docs/` directory contains design docs we authored
for the CLI team (e.g. `cli-graph-output.md` was our original proposal;
`local/track-source-annotate.md` is what they actually implemented).
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,9 @@ When a buffer is closed, the session is exported to `archive/` as
| `:ToolpathStart [actor]` | Start tracking the current buffer. Optional actor override. |
| `:ToolpathStop` | Stop tracking and close the session. |
| `:ToolpathNote <text>` | Annotate the current head step with intent. |
| `:ToolpathAnnotate [text]` | Annotate the head step. Use Lua API for full control. |
| `:ToolpathExport [file]` | Export the Path document. Opens in a scratch buffer if no file given. |
| `:ToolpathStatus` | Print session status: path, actor, step count, undo seq, version. |
| `:ToolpathStatus` | Print session status: path, actor, step count, VCS commit, version. |

## Statusline

Expand Down Expand Up @@ -216,12 +217,30 @@ toolpath.setup(opts) -- configure the plugin
toolpath.start(actor, quiet) -- start tracking current buffer
toolpath.stop(bufnr) -- stop tracking (default: current buffer)
toolpath.note(intent) -- annotate the current head step
toolpath.annotate(opts) -- annotate any step (intent, source, refs)
toolpath.export(output_file) -- export Path document
toolpath.statusline() -- "[toolpath: N steps]" or ""
toolpath.status() -- print session info via vim.notify
toolpath.version -- "0.1.0"
```

## VCS integration

When a tracked file is inside a git repository, the plugin automatically
anchors the session to VCS state:

- **Session start**: `--base-uri` (from git remote) and `--base-ref` (HEAD
commit) are passed to `path track init`, setting `path.base` in the output.
- **Each edit**: `--source '{"type":"git","revision":"...","branch":"..."}'` is
passed to `path track step`, landing in `step.meta.source`.
- **Cache refresh**: git HEAD is cached per repo and refreshed on `FocusGained`
and `ShellCmdPost` — no git calls on every keystroke.

Multiple files in the same repo share `base.ref` and `source.revision` values,
so consumers can correlate their Path documents without session UUIDs.

Files outside a git repo are tracked normally without VCS metadata.

## Undo tree navigation

The plugin correctly handles undo, redo, and undo tree browsers like
Expand Down
69 changes: 66 additions & 3 deletions doc/toolpath.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ CONTENTS *toolpath-contents*
6. Statusline .............................. |toolpath-statusline|
7. Buffer variables ........................ |toolpath-variables|
8. Health check ............................ |toolpath-health|
9. How it works ............................ |toolpath-how-it-works|
9. VCS integration ......................... |toolpath-vcs|
10. How it works ............................ |toolpath-how-it-works|

==============================================================================
1. INTRODUCTION *toolpath-introduction*
Expand Down Expand Up @@ -121,10 +122,16 @@ record ~
If {file} is given, writes to that path. Otherwise opens in a
new scratch buffer with filetype=json.

*:ToolpathAnnotate*
:ToolpathAnnotate [text]
Annotate the current head step. Accepts optional intent text.
For full control (targeting specific steps, attaching source or
refs), use |toolpath.annotate()| from Lua.

*:ToolpathStatus*
:ToolpathStatus
Print current session status: session path, actor, step count,
undo sequence number, and plugin version.
undo sequence number, VCS commit, and plugin version.

==============================================================================
5. LUA API *toolpath-api*
Expand Down Expand Up @@ -152,6 +159,38 @@ toolpath.note({intent})
toolpath.export([output_file])
Export the Path document. Opens in scratch buffer if no file given.

*toolpath.annotate()*
toolpath.annotate({opts})
Annotate a step with metadata. {opts} is a table:

step ~
string (default: head step)
The step ID to annotate.

intent ~
string
Intent text for the step.

source ~
table
VCS source object, e.g. `{ type = "git", revision = "abc123" }`.

refs ~
table[]
Array of ref objects, e.g.
`{{ rel = "issue", href = "https://..." }}`.

Example:
>lua
require("toolpath").annotate({
intent = "fix edge case",
source = { type = "git", revision = "abc123" },
refs = {
{ rel = "issue", href = "https://github.com/org/repo/issues/42" },
},
})
<

*toolpath.statusline()*
toolpath.statusline()
Returns a string for use in statusline components.
Expand Down Expand Up @@ -222,7 +261,31 @@ Run `:checkhealth toolpath` to verify your setup. The health check verifies:
- Reports plugin version

==============================================================================
9. HOW IT WORKS *toolpath-how-it-works*
9. VCS INTEGRATION *toolpath-vcs*

When a tracked file is inside a git repository, the plugin automatically
anchors the session to the current VCS state:

- At session start, `--base-uri` (derived from the git remote) and
`--base-ref` (current HEAD commit) are passed to `path track init`.
This sets `path.base` in the output document.

- On each edit, `--source` is passed to `path track step` with the
cached git HEAD as a JSON `vcsSource` object:
`{"type":"git","revision":"abc123","branch":"main"}`

- The git HEAD cache is refreshed on |FocusGained| and |ShellCmdPost|
events, so commits made in external terminals or via |:!| are picked
up without polling on every keystroke.

When multiple files in the same repo are being tracked, their Path
documents share `base.ref` and `source.revision` values. Consumers can
correlate them by matching these identifiers.

Files not in a git repo are tracked normally without VCS metadata.

==============================================================================
10. HOW IT WORKS *toolpath-how-it-works*

1. |:ToolpathStart| (or auto mode) calls `path track init`, piping the
buffer content as the initial state. The CLI returns a session file path.
Expand Down
Loading