generated from eblume/project-template
feat: wiki-links by id — id-first resolution + heph.nvim [[ picker (§8.4)
Some checks failed
Build / validate (pull_request) Failing after 6m34s
Some checks failed
Build / validate (pull_request) Failing after 6m34s
Backend: `links::resolve_id` now checks for an exact live node id before alias/title, so a canonical `[[NODEID]]` link resolves to its node and can't be shadowed by a like-named node. Legacy `[[Name]]` links still resolve by name (until the migration), so this is additive. heph.nvim: `link.insert` (bound to insert-mode `[[` and `:Heph link`) searches via the `search` RPC and inserts `[[NODEID]]`, with a "+ Create new doc" entry; `<CR>` follow resolves the id directly. e2e covers search→insert→materialize and the create path. Remaining (§8.4): read-expansion/conceal display + the one-time [[Title]]→[[NODEID]] migration (then retire name-resolution + the hack). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a030ad3034
commit
4e8f6743cf
7 changed files with 139 additions and 9 deletions
|
|
@ -25,6 +25,9 @@ M.subs = {
|
|||
follow = function()
|
||||
require("heph.link").follow()
|
||||
end,
|
||||
link = function()
|
||||
require("heph.link").insert()
|
||||
end,
|
||||
open = function(args)
|
||||
if args[1] then
|
||||
require("heph.node").open(args[1])
|
||||
|
|
|
|||
|
|
@ -68,12 +68,48 @@ function M.follow()
|
|||
require("heph.node").open(node.id)
|
||||
end
|
||||
|
||||
--- Attach the buffer-local `<CR>` follow keymap and inline-`#hashtag`
|
||||
--- Pick a node (by full-text search) and insert a canonical `[[NODEID]]` link at
|
||||
--- the cursor — the authoring path for wiki-links-by-id (§8.4); a node id is the
|
||||
--- only thing that ever enters a stored link, so there's no name ambiguity. A
|
||||
--- "Create" entry mints a new doc named after the query. No-op if cancelled.
|
||||
function M.insert()
|
||||
vim.ui.input({ prompt = "Link to: " }, function(query)
|
||||
if not query or query == "" then
|
||||
return
|
||||
end
|
||||
local items = {}
|
||||
for _, hit in ipairs(rpc.call("search", { query = query }) or {}) do
|
||||
items[#items + 1] = hit
|
||||
end
|
||||
items[#items + 1] = { __create = true, title = query }
|
||||
require("heph.picker").select(items, {
|
||||
prompt = "Link to",
|
||||
format = function(it)
|
||||
if it.__create then
|
||||
return "+ Create new doc: " .. it.title
|
||||
end
|
||||
return it.title .. " [" .. (it.kind or "node") .. "]"
|
||||
end,
|
||||
}, function(choice)
|
||||
if not choice then
|
||||
return
|
||||
end
|
||||
local id = choice.__create and rpc.call("node.create", { kind = "doc", title = choice.title }).id or choice.id
|
||||
vim.api.nvim_put({ "[[" .. id .. "]]" }, "c", true, true)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Attach the buffer-local follow/insert keymaps and inline-`#hashtag`
|
||||
--- highlighting (only on heph:// buffers).
|
||||
function M.attach(buf)
|
||||
vim.keymap.set("n", "<CR>", function()
|
||||
M.follow()
|
||||
end, { buffer = buf, desc = "heph: follow [[link]]" })
|
||||
-- Typing `[[` opens the node picker (Obsidian-style), inserting `[[NODEID]]`.
|
||||
vim.keymap.set("i", "[[", function()
|
||||
M.insert()
|
||||
end, { buffer = buf, desc = "heph: insert [[link]]" })
|
||||
|
||||
-- Render inline #hashtags in italics so they stand out — matching the
|
||||
-- save-time tag detection (whitespace-prefixed `#word`, never a `# heading`).
|
||||
|
|
|
|||
73
heph.nvim/tests/e2e/link_insert_spec.lua
Normal file
73
heph.nvim/tests/e2e/link_insert_spec.lua
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
-- Wiki-links by node id (§8.4): the `[[` picker searches nodes and inserts a
|
||||
-- canonical `[[NODEID]]` link; a "Create" entry mints a new doc. The inserted
|
||||
-- id resolves on follow and materializes as a `wiki` link on save.
|
||||
|
||||
local h = require("e2e.helpers")
|
||||
|
||||
describe("link insert picker", function()
|
||||
local ctx
|
||||
before_each(function()
|
||||
ctx = h.start()
|
||||
end)
|
||||
after_each(function()
|
||||
h.stop(ctx)
|
||||
end)
|
||||
|
||||
-- Drive `link.insert` with a stubbed query + choice.
|
||||
local function with_picker(query, pick, fn)
|
||||
vim.g.heph_force_ui_select = true
|
||||
local oi, os = vim.ui.input, vim.ui.select
|
||||
vim.ui.input = function(_o, cb)
|
||||
cb(query)
|
||||
end
|
||||
vim.ui.select = function(items, _o, cb)
|
||||
cb(pick(items))
|
||||
end
|
||||
local ok, err = pcall(fn)
|
||||
vim.ui.input, vim.ui.select, vim.g.heph_force_ui_select = oi, os, nil
|
||||
assert.is_true(ok, tostring(err))
|
||||
end
|
||||
|
||||
it("inserts a canonical [[NODEID]] for a searched node and materializes the link", function()
|
||||
local target = h.create_doc("Roofing", "the roofing doc")
|
||||
local src = h.create_doc("Daily", "")
|
||||
local buf = h.open(src.id)
|
||||
-- Put the cursor on an empty body line below the frontmatter.
|
||||
vim.api.nvim_buf_set_lines(buf, -1, -1, false, { "" })
|
||||
vim.api.nvim_win_set_cursor(0, { vim.api.nvim_buf_line_count(buf), 0 })
|
||||
|
||||
with_picker("roofing", function(items)
|
||||
return items[1] -- the search hit (FTS matches the "roofing" token)
|
||||
end, function()
|
||||
require("heph.link").insert()
|
||||
end)
|
||||
|
||||
-- The buffer now carries `[[<target id>]]`, and saving materializes the link.
|
||||
assert.is_truthy(h.find(buf, "%[%[" .. target.id), "[[id]] not inserted")
|
||||
h.save(buf)
|
||||
local linked = false
|
||||
for _, l in ipairs(ctx.q:call("links.backlinks", { id = target.id })) do
|
||||
if l.src_id == src.id and l.link_type == "wiki" then
|
||||
linked = true
|
||||
end
|
||||
end
|
||||
assert.is_true(linked, "expected a wiki link from src to the picked node")
|
||||
end)
|
||||
|
||||
it("creates a new doc when the Create entry is chosen", function()
|
||||
local src = h.create_doc("Notes", "")
|
||||
local buf = h.open(src.id)
|
||||
vim.api.nvim_buf_set_lines(buf, -1, -1, false, { "" })
|
||||
vim.api.nvim_win_set_cursor(0, { vim.api.nvim_buf_line_count(buf), 0 })
|
||||
|
||||
with_picker("Brand New Topic", function(items)
|
||||
return items[#items] -- the "+ Create" sentinel is last
|
||||
end, function()
|
||||
require("heph.link").insert()
|
||||
end)
|
||||
|
||||
local created = ctx.q:call("node.resolve", { title = "Brand New Topic" })
|
||||
assert.is_truthy(created, "create entry should mint the doc")
|
||||
assert.is_truthy(h.find(buf, "%[%[" .. created.id), "[[new id]] not inserted")
|
||||
end)
|
||||
end)
|
||||
Loading…
Add table
Add a link
Reference in a new issue