Skip to content

empathic/toolpath-nvim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

toolpath.nvim

Neovim plugin for Toolpath — records your editing history as structured provenance documents.

Every edit you make, every undo branch you abandon, captured as a DAG of steps with diffs, actors, and timestamps. When you close the buffer, the session exports a Toolpath Path document.

How it works

Neovim's undo tree is already a DAG. When you undo and make a different edit, it creates a branch — the old branch becomes a dead end. toolpath.nvim maps this directly to Toolpath's step DAG:

              +-- step-3a -- step-4a     (dead end: you undid this)
step-1 -- step-2 --+
              +-- step-3b -- step-4b     (head: where you ended up)

The plugin hooks TextChanged events, tracks undo sequence numbers, and shells out to the path track CLI to compute diffs and build the document incrementally. Normal edits and undo/redo navigations run asynchronously so the UI never blocks. The plugin is thin (~350 lines of Lua) — all the heavy lifting happens in the CLI.

Requirements

  • Neovim >= 0.10 (for vim.system)
  • The path CLI binary on $PATH (or configured via bin)

Installation

lazy.nvim (recommended)

Add a file like lua/plugins/toolpath.lua:

return {
  "empathic/toolpath-nvim",
  cmd = { "ToolpathStart", "ToolpathStatus", "ToolpathExport" },
  event = "BufReadPost",
  config = function()
    require("toolpath").setup({
      actor = "human:alex",
      source = "neovim",
      record = {
        dir = "~/.local/share/toolpath",
      },
    })
  end,
}

event loads the plugin when a file is opened; cmd allows lazy-loading via any :Toolpath* command before that.

Without a plugin manager

Clone into your Neovim runtime path:

git clone https://github.com/empathic/toolpath-nvim \
  ~/.local/share/nvim/site/pack/toolpath/start/toolpath-nvim

Then call setup() in your init.lua:

require("toolpath").setup({
  actor = "human:alex",
  source = "neovim",
  record = {
    dir = "~/.local/share/toolpath",
  },
})

The plugin/toolpath.lua file registers all :Toolpath* commands automatically when Neovim loads the plugin from the runtime path.

Configuration

require("toolpath").setup({
  -- Path to the `path` binary (default: "path", found on $PATH)
  bin = "/usr/local/bin/path",

  -- Default actor for all sessions (default: "human:$USER")
  actor = "human:alex",

  -- Actor definitions — included in the Path document's meta.actors block
  actors = {
    ["human:alex"] = {
      name = "Alex",
      identities = {
        { system = "github", id = "akesling" },
        { system = "email", id = "alex@example.com" },
      },
    },
  },

  -- Path-level metadata
  title = "Editing session",   -- optional, omitted if nil
  source = "neovim",           -- optional, omitted if nil

  -- Session recording
  record = {
    dir = "~/.local/share/toolpath",  -- required: where session files go
    auto = true,                       -- default true; false = manual :ToolpathStart only
    pattern = "*",                     -- default "*"; glob to filter files
  },
})

record options

Key Default Description
dir (required) Base directory for session data. live/ and archive/ subdirs are created automatically. Supports ~.
auto true Auto-start recording when a file is opened. Set false for manual control.
pattern "*" Glob pattern to filter which files are auto-tracked.

Examples:

-- Record everything
record = { dir = "~/.local/share/toolpath" }

-- Only record Rust files
record = { dir = "~/.local/share/toolpath", pattern = "*.rs" }

-- Configure the output directory but start sessions manually
record = { dir = "~/.local/share/toolpath", auto = false }

The directory layout created by the plugin:

~/.local/share/toolpath/
├── README.md         ← explains what's here
├── live/             ← active session state files (temporary)
└── archive/          ← exported Path documents (persistent)
    ├── init.lua.20260217T143022.path.json
    └── main.rs.20260217T150108.path.json

When a buffer is closed, the session is exported to archive/ as <filename>.<timestamp>.path.json and the live/ state file is cleaned up.

Commands

Command Description
: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, VCS commit, version.

Statusline

require("toolpath").statusline() returns "[toolpath: N steps]" when tracking is active, and "" otherwise.

lualine

require("lualine").setup({
  sections = {
    lualine_x = {
      { require("toolpath").statusline },
    },
  },
})

Native statusline

vim.o.statusline = "%f %m%=%{v:lua.require('toolpath').statusline()} %l:%c"

Buffer variables

When tracking is active, the following buffer-local variables are set:

Variable Type Description
vim.b.toolpath_tracking boolean true while tracking
vim.b.toolpath_session string Session state file path
vim.b.toolpath_actor string Actor for this session
vim.b.toolpath_steps number Total steps recorded

All variables are cleared when tracking stops.

Health check

Run :checkhealth toolpath to verify your setup. It checks:

  • Neovim version
  • path binary is found and is the Toolpath CLI
  • Session directory is writable
  • setup() has been called

Lua API

All functions are available on the require("toolpath") module:

local toolpath = require("toolpath")

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 vim-mundo. The Toolpath DAG mirrors the Neovim undo tree — same nodes, same branches, same dead ends.

It distinguishes new edits from navigation by checking whether undotree().seq_last increased:

  • New edit (seq_last increased): computes a diff and creates a Toolpath step. If multiple undo entries were created between TextChanged firings (e.g., by a plugin or macro), intermediate states are replayed to capture each entry individually.
  • Navigation (seq_last unchanged): caches the buffer content via path track visit and inherits the step mapping from the nearest ancestor step (by walking undotree().entries). This means branching from any point in the undo tree — even intermediate states that weren't explicit TextChanged boundaries — wires the parent correctly.

You can freely jump around the undo tree with mundo, undo/redo, g-/g+, or :earlier/:later. Only genuinely new edits produce steps. Branches in the undo tree map to branches in the Toolpath DAG.

What gets recorded

Each Toolpath step contains:

  • A unified diff of the change (computed by the similar crate)
  • The actor who made the change (e.g. human:alex)
  • A timestamp (ISO 8601)
  • Parent references wiring the step into the DAG

The full Path document includes all steps — including dead ends from abandoned undo branches — plus optional metadata (title, source, actor definitions).

Architecture

 Neovim                          CLI
+------------------+     +-------------------+
|  toolpath.nvim   |     |  path track init  |  -- creates session
|                  | --> |  path track step  |  -- records each edit (async)
|  hooks:          |     |  path track visit |  -- caches undo/redo nav (async)
|   TextChanged    |     |  path track note  |  -- annotates intent
|   BufUnload      |     |  path track close |  -- exports + cleans up
|                  |     +-------------------+
+------------------+
        |                         |
        v                         v
  undo tree seq            session state file
  (parent tracking)        (steps, diffs, DAG)

The CLI is editor-agnostic. VS Code, Emacs, or any other editor can use the same path track subcommands.

License

Apache-2.0

About

Neovim Metamemory

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors