local config = require('todo.config') local store = require('todo.store') local views = require('todo.views') ---@class todo.buffer local M = {} ---@type integer? local task_bufnr = nil local task_ns = vim.api.nvim_create_namespace('todo') ---@type 'category'|'priority'|nil local current_view = nil ---@type todo.LineMeta[] local _meta = {} ---@return todo.LineMeta[] function M.meta() return _meta end ---@return integer? function M.bufnr() return task_bufnr end ---@return string? function M.current_view_name() return current_view 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 = 'todo' vim.bo[bufnr].modifiable = true 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 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 /^\S.*$/ contains=taskId syntax match taskPriority /! / contained containedin=taskLine syntax match taskLine /^\/\d\+\/ .*$/ contains=taskId,taskPriority ]]) end) end ---@param bufnr integer local function setup_indentexpr(bufnr) vim.bo[bufnr].indentexpr = 'v:lua.require("todo.buffer").get_indent()' end ---@return integer function M.get_indent() local lnum = vim.v.lnum if lnum <= 1 then return 0 end local prev = vim.fn.getline(lnum - 1) if prev == '' or prev:match('^%S') then return 0 end return 2 end ---@param bufnr integer ---@param line_meta todo.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 if m.due then vim.api.nvim_buf_set_extmark(bufnr, task_ns, row, 0, { virt_text = { { m.due, 'TodoDue' } }, virt_text_pos = 'right_align', }) 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+/')) + 2 or 0 vim.api.nvim_buf_set_extmark(bufnr, task_ns, row, col_start, { end_col = #line, hl_group = 'TodoDone', }) 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 = 'TodoHeader', }) end end end local function setup_highlights() local function hl(name, opts) if vim.fn.hlexists(name) == 0 or vim.tbl_isempty(vim.api.nvim_get_hl(0, { name = name })) then vim.api.nvim_set_hl(0, name, opts) end end hl('TodoHeader', { bold = true }) hl('TodoDue', { fg = '#888888', italic = true }) hl('TodoDone', { strikethrough = true, fg = '#666666' }) hl('TodoPriority', { fg = '#e06c75', bold = true }) 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 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 vim.bo[bufnr].modifiable = true vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) vim.bo[bufnr].modified = false setup_syntax(bufnr) apply_extmarks(bufnr, line_meta) 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_bufnr and vim.api.nvim_buf_is_valid(task_bufnr) then local wins = vim.fn.win_findbuf(task_bufnr) if #wins > 0 then vim.api.nvim_set_current_win(wins[1]) M.render(task_bufnr) return task_bufnr end vim.api.nvim_set_current_buf(task_bufnr) set_win_options(vim.api.nvim_get_current_win()) M.render(task_bufnr) return task_bufnr end task_bufnr = vim.api.nvim_create_buf(true, false) vim.api.nvim_buf_set_name(task_bufnr, 'todo://') set_buf_options(task_bufnr) setup_indentexpr(task_bufnr) vim.api.nvim_set_current_buf(task_bufnr) set_win_options(vim.api.nvim_get_current_win()) M.render(task_bufnr) return task_bufnr end return M