feat(nvim): inline #hashtags become tags on save (§8.3)
Some checks failed
Build / validate (pull_request) Failing after 4m57s

On save, whitespace-prefixed `#hashtags` in a node's body are unioned
into its tag set (via `frontmatter.hashtags` + the existing tag diff), so
you can tag a note by writing `#kitchen` inline. A markdown `# heading`
has a space after the `#`, so it never matches. e2e covers it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-06-03 11:57:25 -07:00
commit 8dc98dc9c1
5 changed files with 54 additions and 9 deletions

View file

@ -79,6 +79,35 @@ local function date_to_ms(s)
return os.time({ year = tonumber(y), month = tonumber(m), day = tonumber(d), hour = 0, min = 0, sec = 0 }) * 1000
end
--- Whitespace-prefixed inline `#hashtags` in `body`, de-duplicated in order.
--- A markdown heading (`# Title`, `## foo`) has a space after the `#`, so it
--- never matches. (Scanned across the whole body, code fences included — a
--- pragmatic v1 simplification.)
function M.hashtags(body)
local seen, out = {}, {}
for tag in (" " .. (body or "")):gmatch("%s#([%w_%-]+)") do
if not seen[tag] then
seen[tag] = true
out[#out + 1] = tag
end
end
return out
end
--- Union of two scalar lists, order-stable, de-duplicated.
local function union(a, b)
local seen, out = {}, {}
for _, list in ipairs({ a or {}, b or {} }) do
for _, x in ipairs(list) do
if not seen[x] then
seen[x] = true
out[#out + 1] = x
end
end
end
return out
end
--- Set difference of two scalar lists → (added, removed).
local function set_diff(old, new)
local oldset, newset = {}, {}
@ -104,12 +133,16 @@ end
--- Apply the difference between `canonical` (rendered on read) and the buffer's
--- `fm` by issuing the matching RPCs. `node_id` is the opened node (title/tags
--- target); task scalars route to the owning task (`fm.task`). Raises on any
--- RPC error (e.g. a mistyped `state` or an unknown `project`).
function M.apply(node_id, canonical, fm)
--- target); task scalars route to the owning task (`fm.task`). Inline
--- `#hashtags` in `body` are unioned into the desired tag set (so they manage
--- tags too); `fm.tags` is updated to that union so the caller caches it as the
--- new canonical. Raises on any RPC error (a mistyped `state`, an unknown
--- `project`).
function M.apply(node_id, canonical, fm, body)
canonical = canonical or {}
-- Tags on the opened node.
-- Tags on the opened node = the frontmatter list inline #hashtags.
fm.tags = union(fm.tags, M.hashtags(body))
local added, removed = set_diff(canonical.tags, fm.tags)
for _, t in ipairs(added) do
rpc.call("tag.add", { node_id = node_id, tag = t })