generated from eblume/project-template
heph.nvim: follow-or-create wiki links + :Heph doc
Some checks failed
Build / validate (pull_request) Failing after 4m13s
Some checks failed
Build / validate (pull_request) Failing after 4m13s
Pressing <CR> on a [[wiki-link]] whose target doesn't exist now creates a doc with that title and opens it (the zettelkasten gesture), and materializes the source's backlink: if the source has unsaved edits, saving re-extracts and links it (and persists the edits); otherwise the wiki link is added directly (a no-op re-save wouldn't re-extract). Adds :Heph doc <title> to create a standalone wiki entry. e2e covers both the saved-source and just-typed-source paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
acb3949896
commit
9249ca46a1
5 changed files with 80 additions and 11 deletions
|
|
@ -18,4 +18,5 @@ Begin the v1 prototype (Phase 1, tech-spec §11.1), built in TDD slices:
|
|||
- `heph.nvim` slice 11b (§8) — task views: `list` is enriched to return titled rows (the same shape as `next`, with the canonical-context id) so the Organizational survey needs no per-row `node.get`. The plugin gains the Tactical **`:Heph next`** and Organizational **`:Heph list`** views (`<CR>` opens a task's canonical-context doc), task **capture**, **set-attention**, **done/drop**, **skip**, and per-task **`log`** append — each resolving "the current task" from the buffer (a task node, or a context doc via its `canonical-context` backlink). A `vim.ui.select` picker (Telescope auto-upgrade when installed) backs `:Heph search`/`capture`/`attention`. Headless e2e adds the capture→next→context→checklist→done workflow and the recurring fresh-checklist workflow (completing a recurring task rolls it forward and the next occurrence presents an all-unchecked checklist).
|
||||
- `heph.nvim` slice 11c (§8) — promotion + CI: `task.promote` mints a committed task from a `- [ ]` context-item line (addressed by its 1-based index) and rewrites that line into a `[[link]]` to the new task; `:Heph promote` does this for the line under the cursor. Wiki-link resolution now excludes a task's canonical-context doc, so `[[Task Title]]` resolves to the task itself (not its identically-titled context doc). The headless e2e suite runs in CI via a Dagger function that bakes a pinned, arch-detected Neovim onto a Rust image and runs the same self-contained suite developers run natively with `mise run test-nvim`; the runner fails on a zero-spec discovery so a misconfigured path can't pass silently.
|
||||
- `heph.nvim` managed daemon — plug-and-play by default: `require("heph").setup({})` spawns and supervises a local `hephd` against the default paths when none is running, kills only the daemon it spawned on exit, and self-heals (respawns + reconnects if the daemon dies mid-session). A daemon you started yourself (a `server`/`client` architecture, or a service) is always respected — the plugin only spawns when nothing is serving the socket; with `autostart = false` it connects only and warns if unreachable. `$HEPH_SOCKET` / `$HEPH_DB` isolate a development Neovim onto a separate daemon + DB.
|
||||
- `heph.nvim` follow-or-create: pressing `<CR>` on a `[[wiki-link]]` whose target doesn't exist yet now **creates** a doc with that title and opens it (the zettelkasten gesture), materializing the source's backlink — so you can link a journal entry to a brand-new note in one keystroke. Plus `:Heph doc <title>` to create a standalone wiki entry.
|
||||
- Dev/installed isolation tooling: a `mise run dev` task runs the working-tree `hephd` on isolated `.dev/` paths, and a how-to ([[install-heph]]) covers installing `heph`/`hephd` from the forge (build-from-source), the lazy.nvim plugin setup, and pointing a dev Neovim at the dev daemon via `$HEPH_SOCKET`/`$HEPH_DB` so it never touches the installed store.
|
||||
|
|
|
|||
|
|
@ -50,14 +50,18 @@ Beyond the existing methods (tech-spec §6), the plugin relies on
|
|||
**`node.resolve {title} → Node | null`**: an exact, owner-scoped,
|
||||
non-tombstoned alias-then-title match — the same mapping the store uses to
|
||||
materialize `wiki` links, so "follow link under cursor" jumps to the *same*
|
||||
node the stored link points at.
|
||||
node the stored link points at. When the target doesn't resolve, follow
|
||||
**creates** a `doc` with that title (the zettelkasten follow-or-create gesture)
|
||||
and materializes the source's backlink (saving the source if it has unsaved
|
||||
edits, else adding the `wiki` link directly).
|
||||
|
||||
## Commands (as of slice 11c)
|
||||
|
||||
| Command | Action |
|
||||
|---|---|
|
||||
| `:Heph today` / `:Heph journal <YYYY-MM-DD>` | Open today's / a dated journal |
|
||||
| `:Heph follow` (also `<CR>` in a node buffer) | Follow the `[[link]]` under the cursor |
|
||||
| `:Heph follow` (also `<CR>` in a node buffer) | Follow the `[[link]]` under the cursor — **creating** the target doc if it doesn't exist yet |
|
||||
| `:Heph doc <title>` | Create (and open) a new wiki doc |
|
||||
| `:Heph open <id>` | Open a node buffer by id |
|
||||
| `:Heph search <query>` | Full-text search; pick a result to open |
|
||||
| `:Heph next [scope]` | Tactical "what is next?" view (`<CR>` opens a task's context) |
|
||||
|
|
|
|||
|
|
@ -23,6 +23,15 @@ M.subs = {
|
|||
require("heph.node").open(args[1])
|
||||
end
|
||||
end,
|
||||
doc = function(args)
|
||||
local title = table.concat(args, " ")
|
||||
if #title == 0 then
|
||||
require("heph.util").notify("usage: :Heph doc <title>", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
local node = require("heph.rpc").call("node.create", { kind = "doc", title = title, body = "" })
|
||||
require("heph.node").open(node.id)
|
||||
end,
|
||||
search = function(args)
|
||||
local query = table.concat(args, " ")
|
||||
if #query == 0 then
|
||||
|
|
|
|||
|
|
@ -36,8 +36,10 @@ function M.target_under_cursor()
|
|||
end
|
||||
end
|
||||
|
||||
--- Follow the `[[link]]` under the cursor to its node. Unresolved links are
|
||||
--- allowed (tech-spec §5) — an INFO toast, not an error.
|
||||
--- Follow the `[[link]]` under the cursor to its node, **creating** the target
|
||||
--- doc if it doesn't exist yet (the zettelkasten follow-or-create gesture). The
|
||||
--- newly-created doc resolves the source's previously-unresolved wiki-link, so
|
||||
--- re-saving the source materializes the backlink.
|
||||
function M.follow()
|
||||
local target = M.target_under_cursor()
|
||||
if not target then
|
||||
|
|
@ -46,8 +48,22 @@ function M.follow()
|
|||
end
|
||||
local node = rpc.call("node.resolve", { title = target })
|
||||
if not node then
|
||||
util.notify("unresolved link [[" .. target .. "]]", vim.log.levels.INFO)
|
||||
return
|
||||
node = rpc.call("node.create", { kind = "doc", title = target, body = "" })
|
||||
util.notify("created [[" .. target .. "]]")
|
||||
-- Materialize the source's wiki-link to the new doc — it was unresolved when
|
||||
-- the source was saved, so extraction skipped it (tech-spec §5). If the
|
||||
-- source has unsaved edits, saving re-extracts and materializes it (and
|
||||
-- persists the edits); otherwise add the link directly (a no-op re-save
|
||||
-- wouldn't re-extract).
|
||||
local src = vim.api.nvim_get_current_buf()
|
||||
local src_id = vim.b[src].heph_node_id
|
||||
if src_id then
|
||||
if vim.bo[src].modified then
|
||||
pcall(require("heph.node").write, src, vim.api.nvim_buf_get_name(src))
|
||||
else
|
||||
pcall(rpc.call, "links.add", { src = src_id, dst = node.id, link_type = "wiki" })
|
||||
end
|
||||
end
|
||||
end
|
||||
require("heph.node").open(node.id)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -29,12 +29,51 @@ describe("follow link", function()
|
|||
assert.are.equal(b.id, vim.b[cur].heph_node_id)
|
||||
end)
|
||||
|
||||
it("leaves an unresolved [[link]] in place without erroring", function()
|
||||
local a = h.create_doc("Lonely", "points to [[Nowhere]]")
|
||||
it("creates the target doc when following an unresolved [[link]]", function()
|
||||
local a = h.create_doc("Daily", "see [[New Topic]]")
|
||||
local buf = h.open(a.id)
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 12 }) -- inside [[Nowhere]]
|
||||
local at = vim.api.nvim_buf_get_lines(buf, 0, 1, false)[1]:find("%[%[New")
|
||||
vim.api.nvim_win_set_cursor(0, { 1, at + 1 }) -- inside [[New Topic]]
|
||||
|
||||
require("heph.link").follow()
|
||||
-- Still on the same buffer; no jump happened.
|
||||
assert.are.equal(buf, vim.api.nvim_get_current_buf())
|
||||
|
||||
-- A new doc titled "New Topic" was created and opened.
|
||||
local created = ctx.q:call("node.resolve", { title = "New Topic" })
|
||||
assert.is_truthy(created, "expected the target doc to be created")
|
||||
assert.are.equal("heph://node/" .. created.id, vim.api.nvim_buf_get_name(0))
|
||||
|
||||
-- ...and the source now backlinks it (the wiki-link materialized).
|
||||
local linked = false
|
||||
for _, l in ipairs(ctx.q:call("links.backlinks", { id = created.id })) do
|
||||
if l.src_id == a.id and l.link_type == "wiki" then
|
||||
linked = true
|
||||
end
|
||||
end
|
||||
assert.is_true(linked, "expected the source to backlink the created doc")
|
||||
end)
|
||||
|
||||
it("creates + links from an unsaved [[link]] just typed into the buffer", function()
|
||||
-- The real gesture: open a note, type a new [[link]], <CR> without :w.
|
||||
local a = h.create_doc("Journalish", "")
|
||||
local buf = h.open(a.id)
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "ref [[Fresh Note]]" })
|
||||
assert.is_true(vim.bo[buf].modified)
|
||||
local at = vim.api.nvim_buf_get_lines(buf, 0, 1, false)[1]:find("%[%[Fresh")
|
||||
vim.api.nvim_win_set_cursor(0, { 1, at + 1 })
|
||||
|
||||
require("heph.link").follow()
|
||||
|
||||
local created = ctx.q:call("node.resolve", { title = "Fresh Note" })
|
||||
assert.is_truthy(created)
|
||||
assert.are.equal("heph://node/" .. created.id, vim.api.nvim_buf_get_name(0))
|
||||
-- The source's pending edit was persisted and the backlink materialized.
|
||||
assert.are.equal("ref [[Fresh Note]]", ctx.q:call("node.get", { id = a.id }).body)
|
||||
local linked = false
|
||||
for _, l in ipairs(ctx.q:call("links.backlinks", { id = created.id })) do
|
||||
if l.src_id == a.id and l.link_type == "wiki" then
|
||||
linked = true
|
||||
end
|
||||
end
|
||||
assert.is_true(linked, "expected the source edit saved and backlink materialized")
|
||||
end)
|
||||
end)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue