generated from eblume/project-template
heph.nvim: :Heph journals — recent-days picker with preview + @create
All checks were successful
Build / validate (pull_request) Successful in 16m0s
All checks were successful
Build / validate (pull_request) Successful in 16m0s
A dailies picker (zkd-style): lists the last `journal_days` (default 7) days newest-first, previews existing journals and shows "@create" for new ones, and opens the chosen day (creating if new). Journals resolve by their ISO-date title, so no new RPC is needed. picker.select gains an optional Telescope preview pane. e2e covers the recent-days list (exists/@create across a month boundary) and open-on-pick via a stubbed vim.ui.select. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e99c284941
commit
d0930aa6a3
7 changed files with 112 additions and 3 deletions
|
|
@ -18,5 +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, and `:Heph home` — a single designated landing/index page (open-or-create by title, configurable via `opts.home`) to grow a map of content around.
|
||||
- `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, and `:Heph home` — a single designated landing/index page (open-or-create by title, configurable via `opts.home`) to grow a map of content around. `:Heph journals` opens a recent-days picker (preview existing days, `@create` for new ones; count via `opts.journal_days`, default 7) — the dailies workflow. Pickers (Telescope) now support a preview pane.
|
||||
- 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.
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ edits, else adding the `wiki` link directly).
|
|||
|---|---|
|
||||
| `:Heph home` | Open the home / index landing page (created on first use; title via `opts.home`) |
|
||||
| `:Heph today` / `:Heph journal <YYYY-MM-DD>` | Open today's / a dated journal |
|
||||
| `:Heph journals` | Pick among recent days (preview existing, `@create` for new); count via `opts.journal_days` |
|
||||
| `: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 |
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ M.subs = {
|
|||
journal = function(args)
|
||||
require("heph.journal").open(args[1])
|
||||
end,
|
||||
journals = function()
|
||||
require("heph.journal").pick()
|
||||
end,
|
||||
follow = function()
|
||||
require("heph.link").follow()
|
||||
end,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ M.defaults = {
|
|||
bin = "hephd",
|
||||
--- Title of the home / index page (`:Heph home`).
|
||||
home = "Home",
|
||||
--- How many recent days the `:Heph journals` picker offers.
|
||||
journal_days = 7,
|
||||
--- Set the default `<leader>h*` keymaps. `false` to opt out.
|
||||
keymaps = true,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,4 +16,47 @@ function M.open(date)
|
|||
return node
|
||||
end
|
||||
|
||||
local function iso_to_time(iso)
|
||||
local y, m, d = iso:match("(%d+)-(%d+)-(%d+)")
|
||||
return os.time({ year = tonumber(y), month = tonumber(m), day = tonumber(d), hour = 12 })
|
||||
end
|
||||
|
||||
--- Entries for the `n` most recent days ending at `today` (ISO; default today),
|
||||
--- newest first. Each: `{ date, node|nil, exists }`. Journals are titled by
|
||||
--- their ISO date, so each day resolves directly.
|
||||
function M.recent_entries(n, today)
|
||||
today = (today and #today > 0) and today or util.iso_today()
|
||||
local t0 = iso_to_time(today)
|
||||
local entries = {}
|
||||
for i = 0, n - 1 do
|
||||
local date = os.date("%Y-%m-%d", t0 - i * 86400)
|
||||
local node = rpc.call("node.resolve", { title = date })
|
||||
entries[#entries + 1] = { date = date, node = node, exists = node ~= nil }
|
||||
end
|
||||
return entries
|
||||
end
|
||||
|
||||
--- Pick among recent journal days — existing days preview their content, new
|
||||
--- days show `@create` — and open the chosen day's journal (creating if new).
|
||||
function M.pick(opts)
|
||||
opts = opts or {}
|
||||
local days = opts.days or (require("heph").config or {}).journal_days or 7
|
||||
require("heph.picker").select(M.recent_entries(days), {
|
||||
prompt = "heph journals",
|
||||
format = function(e)
|
||||
return e.exists and e.date or (e.date .. " @create")
|
||||
end,
|
||||
preview = function(e)
|
||||
if e.exists and e.node and e.node.body and #e.node.body > 0 then
|
||||
return vim.split(e.node.body, "\n", { plain = true })
|
||||
end
|
||||
return { "@create — new journal for " .. e.date }
|
||||
end,
|
||||
}, function(e)
|
||||
if e then
|
||||
M.open(e.date)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -9,18 +9,27 @@ local function telescope_available()
|
|||
return pcall(require, "telescope")
|
||||
end
|
||||
|
||||
--- Select one of `items`. `opts.prompt`, `opts.format(item)->string`.
|
||||
--- Select one of `items`. `opts.prompt`, `opts.format(item)->string`, and an
|
||||
--- optional `opts.preview(item)->lines` (Telescope only; markdown-rendered).
|
||||
--- `on_choice(item|nil, index|nil)` — nil when cancelled.
|
||||
function M.select(items, opts, on_choice)
|
||||
opts = opts or {}
|
||||
if not vim.g.heph_force_ui_select and telescope_available() then
|
||||
-- Telescope path: a thin wrapper so fuzzy UX is available when present.
|
||||
-- (The dropdown is intentionally minimal; richer pickers can come later.)
|
||||
local pickers = require("telescope.pickers")
|
||||
local finders = require("telescope.finders")
|
||||
local conf = require("telescope.config").values
|
||||
local actions = require("telescope.actions")
|
||||
local action_state = require("telescope.actions.state")
|
||||
local previewer = nil
|
||||
if opts.preview then
|
||||
previewer = require("telescope.previewers").new_buffer_previewer({
|
||||
define_preview = function(self, entry)
|
||||
vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, opts.preview(entry.value) or {})
|
||||
vim.bo[self.state.bufnr].filetype = "markdown"
|
||||
end,
|
||||
})
|
||||
end
|
||||
pickers
|
||||
.new({}, {
|
||||
prompt_title = opts.prompt or "heph",
|
||||
|
|
@ -32,6 +41,7 @@ function M.select(items, opts, on_choice)
|
|||
end,
|
||||
}),
|
||||
sorter = conf.generic_sorter({}),
|
||||
previewer = previewer,
|
||||
attach_mappings = function(bufnr)
|
||||
actions.select_default:replace(function()
|
||||
actions.close(bufnr)
|
||||
|
|
|
|||
50
heph.nvim/tests/e2e/journal_picker_spec.lua
Normal file
50
heph.nvim/tests/e2e/journal_picker_spec.lua
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
-- The recent-days journal picker (the `zkd`-style dailies picker).
|
||||
|
||||
local h = require("e2e.helpers")
|
||||
|
||||
describe("journal picker", function()
|
||||
local ctx
|
||||
before_each(function()
|
||||
ctx = h.start()
|
||||
end)
|
||||
after_each(function()
|
||||
h.stop(ctx)
|
||||
end)
|
||||
|
||||
it("lists recent days newest-first with @create state", function()
|
||||
local entries = require("heph.journal").recent_entries(5, "2026-06-02")
|
||||
assert.are.equal(5, #entries)
|
||||
assert.are.equal("2026-06-02", entries[1].date) -- newest first
|
||||
assert.are.equal("2026-05-29", entries[5].date) -- crosses the month boundary
|
||||
for _, e in ipairs(entries) do
|
||||
assert.is_false(e.exists) -- nothing created yet
|
||||
end
|
||||
|
||||
-- Create one day's journal; it now reports as existing (with its node).
|
||||
ctx.q:call("journal.open_or_create", { date = "2026-05-31" })
|
||||
local found
|
||||
for _, e in ipairs(require("heph.journal").recent_entries(5, "2026-06-02")) do
|
||||
if e.date == "2026-05-31" then
|
||||
found = e
|
||||
end
|
||||
end
|
||||
assert.is_true(found.exists)
|
||||
assert.is_truthy(found.node)
|
||||
end)
|
||||
|
||||
it("opens the picked day's journal", function()
|
||||
vim.g.heph_force_ui_select = true
|
||||
local orig, picked = vim.ui.select, nil
|
||||
vim.ui.select = function(items, _opts, on_choice)
|
||||
picked = items[1] -- choose the newest day
|
||||
on_choice(items[1])
|
||||
end
|
||||
require("heph.journal").pick()
|
||||
vim.ui.select, vim.g.heph_force_ui_select = orig, nil
|
||||
|
||||
assert.is_truthy(picked)
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
assert.are.equal("journal", vim.b[buf].heph_node_kind)
|
||||
assert.are.equal(picked.date, ctx.q:call("node.get", { id = vim.b[buf].heph_node_id }).title)
|
||||
end)
|
||||
end)
|
||||
Loading…
Add table
Add a link
Reference in a new issue