From d0930aa6a3e4c4ac2f6821d3e24299b2741e70c0 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Tue, 2 Jun 2026 11:26:45 -0700 Subject: [PATCH] =?UTF-8?q?heph.nvim:=20:Heph=20journals=20=E2=80=94=20rec?= =?UTF-8?q?ent-days=20picker=20with=20preview=20+=20@create?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/changelog.d/v1-prototype.feature.md | 2 +- docs/reference/heph-nvim.md | 1 + heph.nvim/lua/heph/command.lua | 3 ++ heph.nvim/lua/heph/config.lua | 2 + heph.nvim/lua/heph/journal.lua | 43 ++++++++++++++++++ heph.nvim/lua/heph/picker.lua | 14 +++++- heph.nvim/tests/e2e/journal_picker_spec.lua | 50 +++++++++++++++++++++ 7 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 heph.nvim/tests/e2e/journal_picker_spec.lua diff --git a/docs/changelog.d/v1-prototype.feature.md b/docs/changelog.d/v1-prototype.feature.md index fa6eae6..33977e2 100644 --- a/docs/changelog.d/v1-prototype.feature.md +++ b/docs/changelog.d/v1-prototype.feature.md @@ -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 (`` 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 `` 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 ` 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. diff --git a/docs/reference/heph-nvim.md b/docs/reference/heph-nvim.md index 669315f..59db74e 100644 --- a/docs/reference/heph-nvim.md +++ b/docs/reference/heph-nvim.md @@ -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 | diff --git a/heph.nvim/lua/heph/command.lua b/heph.nvim/lua/heph/command.lua index 3eeecad..af63122 100644 --- a/heph.nvim/lua/heph/command.lua +++ b/heph.nvim/lua/heph/command.lua @@ -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, diff --git a/heph.nvim/lua/heph/config.lua b/heph.nvim/lua/heph/config.lua index 1ad9da6..0571b40 100644 --- a/heph.nvim/lua/heph/config.lua +++ b/heph.nvim/lua/heph/config.lua @@ -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, } diff --git a/heph.nvim/lua/heph/journal.lua b/heph.nvim/lua/heph/journal.lua index 389ab81..691ad5a 100644 --- a/heph.nvim/lua/heph/journal.lua +++ b/heph.nvim/lua/heph/journal.lua @@ -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 diff --git a/heph.nvim/lua/heph/picker.lua b/heph.nvim/lua/heph/picker.lua index b7726db..de477c9 100644 --- a/heph.nvim/lua/heph/picker.lua +++ b/heph.nvim/lua/heph/picker.lua @@ -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) diff --git a/heph.nvim/tests/e2e/journal_picker_spec.lua b/heph.nvim/tests/e2e/journal_picker_spec.lua new file mode 100644 index 0000000..4b171aa --- /dev/null +++ b/heph.nvim/tests/e2e/journal_picker_spec.lua @@ -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)