feat: add markdown detail buffer for task notes (#162)
Problem: tasks only have a one-line description. There is no way to attach extended notes, checklists, or context to a task. Solution: add `ge` keymap to open a `pending://task/<id>` markdown buffer that replaces the task list in the same split. The buffer shows a read-only metadata header (status, priority, category, due, recurrence) rendered via extmarks, a `---` separator, and editable notes below. `:w` saves notes to a new top-level `notes` field on the task stored in the single `tasks.json`. `q` returns to the task list.
This commit is contained in:
parent
ba8b550b8c
commit
20506af849
7 changed files with 298 additions and 0 deletions
|
|
@ -584,6 +584,7 @@ local function setup_highlights()
|
|||
vim.api.nvim_set_hl(0, 'PendingFilter', { link = 'DiagnosticWarn', default = true })
|
||||
vim.api.nvim_set_hl(0, 'PendingForge', { link = 'DiagnosticInfo', default = true })
|
||||
vim.api.nvim_set_hl(0, 'PendingForgeClosed', { link = 'Comment', default = true })
|
||||
vim.api.nvim_set_hl(0, 'PendingDetailMeta', { link = 'Comment', default = true })
|
||||
end
|
||||
|
||||
---@return string
|
||||
|
|
@ -805,4 +806,213 @@ function M.open()
|
|||
return task_bufnr
|
||||
end
|
||||
|
||||
local ns_detail = vim.api.nvim_create_namespace('pending_detail')
|
||||
local DETAIL_SEPARATOR = '---'
|
||||
|
||||
---@type integer?
|
||||
local _detail_bufnr = nil
|
||||
---@type integer?
|
||||
local _detail_task_id = nil
|
||||
|
||||
---@return integer?
|
||||
function M.detail_bufnr()
|
||||
return _detail_bufnr
|
||||
end
|
||||
|
||||
---@return integer?
|
||||
function M.detail_task_id()
|
||||
return _detail_task_id
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param task pending.Task
|
||||
---@return nil
|
||||
local function apply_detail_extmarks(bufnr, task)
|
||||
vim.api.nvim_buf_clear_namespace(bufnr, ns_detail, 0, -1)
|
||||
local icons = config.get().icons
|
||||
local parts = {}
|
||||
local status_label = task.status or 'pending'
|
||||
local icon_char = icons[status_label] or icons.pending
|
||||
table.insert(parts, { 'Status: [' .. icon_char .. '] ' .. status_label, 'PendingDetailMeta' })
|
||||
if task.priority and task.priority > 0 then
|
||||
table.insert(parts, { ' ', 'Normal' })
|
||||
table.insert(
|
||||
parts,
|
||||
{ 'Priority: ' .. string.rep(icons.priority, task.priority), 'PendingDetailMeta' }
|
||||
)
|
||||
end
|
||||
local line2 = {}
|
||||
if task.category then
|
||||
table.insert(line2, { 'Category: ' .. task.category, 'PendingDetailMeta' })
|
||||
end
|
||||
if task.due then
|
||||
if #line2 > 0 then
|
||||
table.insert(line2, { ' ', 'Normal' })
|
||||
end
|
||||
local due_label = task.due
|
||||
local y, mo, d = task.due:match('^(%d%d%d%d)%-(%d%d)%-(%d%d)')
|
||||
if y then
|
||||
local t = os.time({
|
||||
year = tonumber(y) --[[@as integer]],
|
||||
month = tonumber(mo) --[[@as integer]],
|
||||
day = tonumber(d) --[[@as integer]],
|
||||
})
|
||||
due_label = os.date(config.get().date_format, t) --[[@as string]]
|
||||
end
|
||||
table.insert(line2, { 'Due: ' .. due_label, 'PendingDetailMeta' })
|
||||
end
|
||||
if task.recur then
|
||||
if #line2 > 0 then
|
||||
table.insert(line2, { ' ', 'Normal' })
|
||||
end
|
||||
table.insert(line2, { 'Recur: ' .. task.recur, 'PendingDetailMeta' })
|
||||
end
|
||||
if #parts > 0 then
|
||||
vim.api.nvim_buf_set_extmark(bufnr, ns_detail, 1, 0, {
|
||||
virt_text = parts,
|
||||
virt_text_pos = 'overlay',
|
||||
})
|
||||
end
|
||||
if #line2 > 0 then
|
||||
vim.api.nvim_buf_set_extmark(bufnr, ns_detail, 2, 0, {
|
||||
virt_text = line2,
|
||||
virt_text_pos = 'overlay',
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
---@param task_id integer
|
||||
---@return integer? bufnr
|
||||
function M.open_detail(task_id)
|
||||
if not _store then
|
||||
return nil
|
||||
end
|
||||
if _detail_bufnr and vim.api.nvim_buf_is_valid(_detail_bufnr) then
|
||||
if _detail_task_id == task_id then
|
||||
return _detail_bufnr
|
||||
end
|
||||
vim.api.nvim_buf_delete(_detail_bufnr, { force = true })
|
||||
_detail_bufnr = nil
|
||||
_detail_task_id = nil
|
||||
end
|
||||
local task = _store:get(task_id)
|
||||
if not task then
|
||||
log.warn('task not found: ' .. task_id)
|
||||
return nil
|
||||
end
|
||||
|
||||
setup_highlights()
|
||||
|
||||
local bufnr = vim.api.nvim_create_buf(true, false)
|
||||
vim.api.nvim_buf_set_name(bufnr, 'pending://task/' .. task_id)
|
||||
vim.bo[bufnr].buftype = 'acwrite'
|
||||
vim.bo[bufnr].filetype = 'markdown'
|
||||
vim.bo[bufnr].swapfile = false
|
||||
|
||||
local lines = {
|
||||
'# ' .. task.description,
|
||||
'',
|
||||
'',
|
||||
DETAIL_SEPARATOR,
|
||||
}
|
||||
local notes = task.notes or ''
|
||||
if notes ~= '' then
|
||||
for note_line in (notes .. '\n'):gmatch('(.-)\n') do
|
||||
table.insert(lines, note_line)
|
||||
end
|
||||
else
|
||||
table.insert(lines, '')
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||
vim.bo[bufnr].modified = false
|
||||
|
||||
apply_detail_extmarks(bufnr, task)
|
||||
|
||||
local winid = task_winid
|
||||
if winid and vim.api.nvim_win_is_valid(winid) then
|
||||
vim.api.nvim_win_set_buf(winid, bufnr)
|
||||
end
|
||||
|
||||
vim.wo[winid].conceallevel = 0
|
||||
vim.wo[winid].foldmethod = 'manual'
|
||||
vim.wo[winid].foldenable = false
|
||||
|
||||
_detail_bufnr = bufnr
|
||||
_detail_task_id = task_id
|
||||
|
||||
local separator_row = 3
|
||||
local cursor_row = separator_row + 2
|
||||
local total = vim.api.nvim_buf_line_count(bufnr)
|
||||
if cursor_row > total then
|
||||
cursor_row = total
|
||||
end
|
||||
pcall(vim.api.nvim_win_set_cursor, winid, { cursor_row, 0 })
|
||||
|
||||
return bufnr
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.close_detail()
|
||||
if _detail_bufnr and vim.api.nvim_buf_is_valid(_detail_bufnr) then
|
||||
vim.api.nvim_buf_delete(_detail_bufnr, { force = true })
|
||||
end
|
||||
_detail_bufnr = nil
|
||||
_detail_task_id = nil
|
||||
|
||||
if task_winid and vim.api.nvim_win_is_valid(task_winid) then
|
||||
if task_bufnr and vim.api.nvim_buf_is_valid(task_bufnr) then
|
||||
vim.api.nvim_win_set_buf(task_winid, task_bufnr)
|
||||
set_win_options(task_winid)
|
||||
M.render(task_bufnr)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.save_detail()
|
||||
if not _detail_bufnr or not _detail_task_id or not _store then
|
||||
return
|
||||
end
|
||||
local task = _store:get(_detail_task_id)
|
||||
if not task then
|
||||
log.warn('task was deleted')
|
||||
M.close_detail()
|
||||
return
|
||||
end
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(_detail_bufnr, 0, -1, false)
|
||||
|
||||
local sep_row = nil
|
||||
for i, line in ipairs(lines) do
|
||||
if line == DETAIL_SEPARATOR then
|
||||
sep_row = i
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local notes_text = ''
|
||||
if sep_row and sep_row < #lines then
|
||||
local note_lines = {}
|
||||
for i = sep_row + 1, #lines do
|
||||
table.insert(note_lines, lines[i])
|
||||
end
|
||||
notes_text = table.concat(note_lines, '\n')
|
||||
notes_text = notes_text:gsub('%s+$', '')
|
||||
end
|
||||
|
||||
if notes_text == '' then
|
||||
_store:update(_detail_task_id, { notes = vim.NIL })
|
||||
else
|
||||
_store:update(_detail_task_id, { notes = notes_text })
|
||||
end
|
||||
_store:save()
|
||||
|
||||
vim.bo[_detail_bufnr].modified = false
|
||||
local updated = _store:get(_detail_task_id)
|
||||
if updated then
|
||||
apply_detail_extmarks(_detail_bufnr, updated)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue