local config = require('pending.config') local store = require('pending.store') local views = require('pending.views') ---@class pending.buffer local M = {} ---@type integer? local task_bufnr = nil ---@type integer? local task_winid = nil local task_ns = vim.api.nvim_create_namespace('pending') ---@type 'category'|'priority'|nil local current_view = nil ---@type pending.LineMeta[] local _meta = {} ---@type table> local _fold_state = {} ---@return pending.LineMeta[] function M.meta() return _meta end ---@return integer? function M.bufnr() return task_bufnr end ---@return integer? function M.winid() return task_winid end ---@return string? function M.current_view_name() return current_view end function M.clear_winid() task_winid = nil end function M.close() if task_winid and vim.api.nvim_win_is_valid(task_winid) then vim.api.nvim_win_close(task_winid, false) end task_winid = nil end ---@param bufnr integer local function set_buf_options(bufnr) vim.bo[bufnr].buftype = 'acwrite' vim.bo[bufnr].bufhidden = 'hide' vim.bo[bufnr].swapfile = false vim.bo[bufnr].filetype = 'pending' vim.bo[bufnr].modifiable = true vim.bo[bufnr].omnifunc = 'v:lua.require("pending.complete").omnifunc' end ---@param winid integer local function set_win_options(winid) vim.wo[winid].conceallevel = 3 vim.wo[winid].concealcursor = 'nvic' vim.wo[winid].wrap = false vim.wo[winid].number = false vim.wo[winid].relativenumber = false vim.wo[winid].signcolumn = 'no' vim.wo[winid].foldcolumn = '0' vim.wo[winid].spell = false vim.wo[winid].cursorline = true vim.wo[winid].winfixheight = true end ---@param bufnr integer local function setup_syntax(bufnr) vim.api.nvim_buf_call(bufnr, function() vim.cmd([[ syntax clear syntax match taskId /^\/\d\+\// conceal syntax match taskHeader /^## .*$/ contains=taskId syntax match taskCheckbox /\[!\]/ contained containedin=taskLine syntax match taskLine /^\/\d\+\/- \[.\] .*$/ contains=taskId,taskCheckbox ]]) end) end ---@param above boolean function M.open_line(above) local bufnr = task_bufnr if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then return end local row = vim.api.nvim_win_get_cursor(0)[1] local insert_row = above and (row - 1) or row vim.bo[bufnr].modifiable = true vim.api.nvim_buf_set_lines(bufnr, insert_row, insert_row, false, { '- [ ] ' }) vim.api.nvim_win_set_cursor(0, { insert_row + 1, 6 }) vim.cmd('startinsert!') end ---@return string function M.get_fold() local lnum = vim.v.lnum local m = _meta[lnum] if not m then return '0' end if m.type == 'header' then return '>1' elseif m.type == 'task' then return '1' else return '0' end end ---@param bufnr integer ---@param line_meta pending.LineMeta[] local function apply_extmarks(bufnr, line_meta) vim.api.nvim_buf_clear_namespace(bufnr, task_ns, 0, -1) for i, m in ipairs(line_meta) do local row = i - 1 if m.type == 'task' then local due_hl = m.overdue and 'PendingOverdue' or 'PendingDue' local virt_parts = {} if m.show_category and m.category then table.insert(virt_parts, { m.category, 'PendingHeader' }) end if m.recur then table.insert(virt_parts, { '\u{21bb} ' .. m.recur, 'PendingRecur' }) end if m.due then table.insert(virt_parts, { m.due, due_hl }) end if #virt_parts > 0 then for p = 1, #virt_parts - 1 do virt_parts[p][1] = virt_parts[p][1] .. ' ' end vim.api.nvim_buf_set_extmark(bufnr, task_ns, row, 0, { virt_text = virt_parts, virt_text_pos = 'eol', }) end if m.status == 'done' then local line = vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1] or '' local col_start = line:find('/%d+/') and select(2, line:find('/%d+/')) or 0 vim.api.nvim_buf_set_extmark(bufnr, task_ns, row, col_start, { end_col = #line, hl_group = 'PendingDone', }) end elseif m.type == 'header' then local line = vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1] or '' vim.api.nvim_buf_set_extmark(bufnr, task_ns, row, 0, { end_col = #line, hl_group = 'PendingHeader', }) end end end local function setup_highlights() vim.api.nvim_set_hl(0, 'PendingHeader', { link = 'Title', default = true }) vim.api.nvim_set_hl(0, 'PendingDue', { link = 'DiagnosticHint', default = true }) vim.api.nvim_set_hl(0, 'PendingOverdue', { link = 'DiagnosticError', default = true }) vim.api.nvim_set_hl(0, 'PendingDone', { link = 'Comment', default = true }) vim.api.nvim_set_hl(0, 'PendingPriority', { link = 'DiagnosticWarn', default = true }) vim.api.nvim_set_hl(0, 'PendingRecur', { link = 'DiagnosticInfo', default = true }) end local function snapshot_folds(bufnr) if current_view ~= 'category' then return end for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do local state = {} vim.api.nvim_win_call(winid, function() for lnum, m in ipairs(_meta) do if m.type == 'header' and m.category then if vim.fn.foldclosed(lnum) ~= -1 then state[m.category] = true end end end end) _fold_state[winid] = state end end local function restore_folds(bufnr) if current_view ~= 'category' then return end for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do local state = _fold_state[winid] if state and next(state) ~= nil then vim.api.nvim_win_call(winid, function() vim.cmd('normal! zx') local saved = vim.api.nvim_win_get_cursor(0) for lnum, m in ipairs(_meta) do if m.type == 'header' and m.category and state[m.category] then vim.api.nvim_win_set_cursor(0, { lnum, 0 }) vim.cmd('normal! zc') end end vim.api.nvim_win_set_cursor(0, saved) end) _fold_state[winid] = nil end end end ---@param bufnr? integer function M.render(bufnr) bufnr = bufnr or task_bufnr if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then return end current_view = current_view or config.get().default_view vim.api.nvim_buf_set_name(bufnr, 'pending://' .. current_view) local tasks = store.active_tasks() local lines, line_meta if current_view == 'priority' then lines, line_meta = views.priority_view(tasks) else lines, line_meta = views.category_view(tasks) end _meta = line_meta snapshot_folds(bufnr) vim.bo[bufnr].modifiable = true local saved = vim.bo[bufnr].undolevels vim.bo[bufnr].undolevels = -1 vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) vim.bo[bufnr].modified = false vim.bo[bufnr].undolevels = saved setup_syntax(bufnr) apply_extmarks(bufnr, line_meta) for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do if current_view == 'category' then vim.wo[winid].foldmethod = 'expr' vim.wo[winid].foldexpr = 'v:lua.require("pending.buffer").get_fold()' vim.wo[winid].foldlevel = 99 vim.wo[winid].foldenable = true else vim.wo[winid].foldmethod = 'manual' vim.wo[winid].foldenable = false end end restore_folds(bufnr) end function M.toggle_view() if current_view == 'category' then current_view = 'priority' else current_view = 'category' end M.render() end ---@return integer bufnr function M.open() setup_highlights() store.load() if task_winid and vim.api.nvim_win_is_valid(task_winid) then vim.api.nvim_set_current_win(task_winid) M.render(task_bufnr) return task_bufnr --[[@as integer]] end if not (task_bufnr and vim.api.nvim_buf_is_valid(task_bufnr)) then task_bufnr = vim.api.nvim_create_buf(true, false) set_buf_options(task_bufnr) end vim.cmd('botright new') task_winid = vim.api.nvim_get_current_win() vim.api.nvim_win_set_buf(task_winid, task_bufnr) local h = config.get().drawer_height if h and h > 0 then vim.api.nvim_win_set_height(task_winid, h) end set_win_options(task_winid) M.render(task_bufnr) return task_bufnr end return M