diff --git a/lua/pending/buffer.lua b/lua/pending/buffer.lua index b60d633..2036357 100644 --- a/lua/pending/buffer.lua +++ b/lua/pending/buffer.lua @@ -56,9 +56,9 @@ local function setup_syntax(bufnr) 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 + syntax match taskHeader /^\S.*$/ contains=taskId + syntax match taskPriority /\[\d\+\] / contained containedin=taskLine + syntax match taskLine /^\/\d\+\/ .*$/ contains=taskId,taskPriority ]]) end) end @@ -72,8 +72,8 @@ function M.open_line(above) 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.api.nvim_buf_set_lines(bufnr, insert_row, insert_row, false, { ' ' }) + vim.api.nvim_win_set_cursor(0, { insert_row + 1, 2 }) vim.cmd('startinsert!') end @@ -113,18 +113,18 @@ local function apply_extmarks(bufnr, line_meta) if virt_text then vim.api.nvim_buf_set_extmark(bufnr, task_ns, row, 0, { virt_text = virt_text, - virt_text_pos = 'eol', + virt_text_pos = 'right_align', }) end elseif m.due then vim.api.nvim_buf_set_extmark(bufnr, task_ns, row, 0, { virt_text = { { m.due, due_hl } }, - virt_text_pos = 'eol', + 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+/')) or 0 + 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 = 'PendingDone', @@ -168,11 +168,8 @@ function M.render(bufnr) _meta = line_meta 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) diff --git a/lua/pending/config.lua b/lua/pending/config.lua index 2e647e4..d137acb 100644 --- a/lua/pending/config.lua +++ b/lua/pending/config.lua @@ -18,7 +18,7 @@ local M = {} local defaults = { data_path = vim.fn.stdpath('data') .. '/pending/tasks.json', default_view = 'category', - default_category = 'Todo', + default_category = 'Inbox', date_format = '%b %d', date_syntax = 'due', category_order = {}, diff --git a/lua/pending/diff.lua b/lua/pending/diff.lua index 85f083c..1107b31 100644 --- a/lua/pending/diff.lua +++ b/lua/pending/diff.lua @@ -7,7 +7,6 @@ local store = require('pending.store') ---@field id? integer ---@field description? string ---@field priority? integer ----@field status? string ---@field category? string ---@field due? string ---@field lnum integer @@ -27,17 +26,20 @@ function M.parse_buffer(lines) local current_category = nil for i, line in ipairs(lines) do - local id, body = line:match('^/(%d+)/(- %[.%] .*)$') + local id, body = line:match('^/(%d+)/( .+)$') if not id then - body = line:match('^(- %[.%] .*)$') + body = line:match('^( .+)$') end if line == '' then table.insert(result, { type = 'blank', lnum = i }) elseif id or body then - local stripped = body:match('^- %[.%] (.*)$') or body - local state_char = body:match('^- %[(.-)%]') or ' ' - local priority = state_char == '!' and 1 or 0 - local status = state_char == 'x' and 'done' or 'pending' + local stripped = body:match('^ (.+)$') or body + local prio_str = stripped:match('^%[(%d+)%] ') + local priority = 0 + if prio_str then + priority = tonumber(prio_str) + stripped = stripped:sub(#prio_str + 4) + end local description, metadata = parse.body(stripped) if description and description ~= '' then table.insert(result, { @@ -45,15 +47,14 @@ function M.parse_buffer(lines) id = id and tonumber(id) or nil, description = description, priority = priority, - status = status, category = metadata.cat or current_category or config.get().default_category, due = metadata.due, lnum = i, }) end - elseif line:match('^## (.+)$') then - current_category = line:match('^## (.+)$') - table.insert(result, { type = 'header', category = current_category, lnum = i }) + elseif line:match('^%S') then + current_category = line + table.insert(result, { type = 'header', category = line, lnum = i }) end end @@ -112,15 +113,6 @@ function M.apply(lines) task.due = entry.due changed = true end - if entry.status and task.status ~= entry.status then - task.status = entry.status - if entry.status == 'done' then - task['end'] = now - else - task['end'] = nil - end - changed = true - end if task.order ~= order_counter then task.order = order_counter changed = true diff --git a/lua/pending/init.lua b/lua/pending/init.lua index ec69d89..1593bc5 100644 --- a/lua/pending/init.lua +++ b/lua/pending/init.lua @@ -52,8 +52,17 @@ function M._setup_buf_mappings(bufnr) vim.keymap.set('n', 'g?', function() M.show_help() end, opts) - vim.keymap.set('n', '!', function() - M.toggle_priority() + vim.keymap.set('n', '', function() + M.change_priority(1) + end, opts) + vim.keymap.set('n', '', function() + M.change_priority(-1) + end, opts) + vim.keymap.set('v', 'g', function() + M.change_priority_visual(1) + end, opts) + vim.keymap.set('v', 'g', function() + M.change_priority_visual(-1) end, opts) vim.keymap.set('n', 'D', function() M.prompt_date() @@ -117,15 +126,10 @@ function M.toggle_complete() end store.save() buffer.render(bufnr) - for lnum, m in ipairs(buffer.meta()) do - if m.id == id then - vim.api.nvim_win_set_cursor(0, { lnum, 0 }) - break - end - end end -function M.toggle_priority() +---@param delta integer +function M.change_priority(delta) local bufnr = buffer.bufnr() if not bufnr then return @@ -143,7 +147,7 @@ function M.toggle_priority() if not task then return end - local new_priority = task.priority > 0 and 0 or 1 + local new_priority = math.max(0, task.priority + delta) store.update(id, { priority = new_priority }) store.save() buffer.render(bufnr) @@ -155,6 +159,33 @@ function M.toggle_priority() end end +---@param delta integer +function M.change_priority_visual(delta) + local bufnr = buffer.bufnr() + if not bufnr then + return + end + local start_row = vim.fn.line("'<") + local end_row = vim.fn.line("'>") + local meta = buffer.meta() + local changed = false + for row = start_row, end_row do + local m = meta[row] + if m and m.type == 'task' and m.id then + local task = store.get(m.id) + if task then + local new_priority = math.max(0, task.priority + delta) + store.update(m.id, { priority = new_priority }) + changed = true + end + end + end + if changed then + store.save() + buffer.render(bufnr) + end +end + function M.prompt_date() local bufnr = buffer.bufnr() if not bufnr then @@ -311,7 +342,10 @@ function M.show_help() '', ' Toggle complete/uncomplete', ' Switch category/priority view', - '! Toggle urgent', + ' Raise priority level', + ' Lower priority level', + 'g Raise priority for visual selection', + 'g Lower priority for visual selection', 'D Set due date', 'U Undo last write', 'o / O Add new task line', @@ -337,7 +371,7 @@ function M.show_help() '', 'Highlights:', ' PendingOverdue overdue tasks (red)', - ' PendingPriority [!] urgent tasks', + ' PendingPriority [N] priority prefix', '', 'Press q or to close', } diff --git a/lua/pending/views.lua b/lua/pending/views.lua index 7bcfaca..84567e9 100644 --- a/lua/pending/views.lua +++ b/lua/pending/views.lua @@ -125,7 +125,7 @@ function M.category_view(tasks) table.insert(lines, '') table.insert(meta, { type = 'blank' }) end - table.insert(lines, '## ' .. cat) + table.insert(lines, cat) table.insert(meta, { type = 'header', category = cat }) local all = {} @@ -138,8 +138,9 @@ function M.category_view(tasks) for _, task in ipairs(all) do local prefix = '/' .. task.id .. '/' - local state = task.status == 'done' and 'x' or (task.priority > 0 and '!' or ' ') - local line = prefix .. '- [' .. state .. '] ' .. task.description + local indent = ' ' + local prio = task.priority > 0 and ('[' .. task.priority .. '] ') or '' + local line = prefix .. indent .. prio .. task.description table.insert(lines, line) table.insert(meta, { type = 'task', @@ -188,8 +189,9 @@ function M.priority_view(tasks) for _, task in ipairs(all) do local prefix = '/' .. task.id .. '/' - local state = task.status == 'done' and 'x' or (task.priority > 0 and '!' or ' ') - local line = prefix .. '- [' .. state .. '] ' .. task.description + local indent = ' ' + local prio = task.priority == 1 and '! ' or '' + local line = prefix .. indent .. prio .. task.description table.insert(lines, line) table.insert(meta, { type = 'task', diff --git a/plugin/pending.lua b/plugin/pending.lua index 465ee65..56dedc3 100644 --- a/plugin/pending.lua +++ b/plugin/pending.lua @@ -8,7 +8,7 @@ vim.api.nvim_create_user_command('Pending', function(opts) end, { nargs = '*', complete = function(arg_lead, cmd_line) - local subcmds = { 'add', 'sync', 'archive', 'due', 'undo' } + local subcmds = { 'add', 'sync', 'archive' } if not cmd_line:match('^Pending%s+%S') then return vim.tbl_filter(function(s) return s:find(arg_lead, 1, true) == 1 @@ -30,8 +30,12 @@ vim.keymap.set('n', '(pending-view)', function() require('pending.buffer').toggle_view() end) -vim.keymap.set('n', '(pending-priority)', function() - require('pending').toggle_priority() +vim.keymap.set('n', '(pending-priority-up)', function() + require('pending').change_priority(1) +end) + +vim.keymap.set('n', '(pending-priority-down)', function() + require('pending').change_priority(-1) end) vim.keymap.set('n', '(pending-date)', function() diff --git a/spec/diff_spec.lua b/spec/diff_spec.lua index 7e7a9cb..b8fcfd9 100644 --- a/spec/diff_spec.lua +++ b/spec/diff_spec.lua @@ -25,12 +25,12 @@ describe('diff', function() describe('parse_buffer', function() it('parses headers and tasks', function() local lines = { - '## School', - '/1/- [ ] Do homework', - '/2/- [!] Read chapter 5', + 'School', + '/1/ Do homework', + '/2/ ! Read chapter 5', '', - '## Errands', - '/3/- [ ] Buy groceries', + 'Errands', + '/3/ Buy groceries', } local result = diff.parse_buffer(lines) assert.are.equal(6, #result) @@ -48,8 +48,8 @@ describe('diff', function() it('handles new tasks without ids', function() local lines = { - '## Inbox', - '- [ ] New task here', + 'Inbox', + ' New task here', } local result = diff.parse_buffer(lines) assert.are.equal(2, #result) @@ -62,9 +62,9 @@ describe('diff', function() describe('apply', function() it('creates new tasks from buffer lines', function() local lines = { - '## Inbox', - '- [ ] First task', - '- [ ] Second task', + 'Inbox', + ' First task', + ' Second task', } diff.apply(lines) store.unload() @@ -80,8 +80,8 @@ describe('diff', function() store.add({ description = 'Delete me' }) store.save() local lines = { - '## Inbox', - '/1/- [ ] Keep me', + 'Inbox', + '/1/ Keep me', } diff.apply(lines) store.unload() @@ -97,8 +97,8 @@ describe('diff', function() store.add({ description = 'Original' }) store.save() local lines = { - '## Inbox', - '/1/- [ ] Renamed', + 'Inbox', + '/1/ Renamed', } diff.apply(lines) store.unload() @@ -111,9 +111,9 @@ describe('diff', function() store.add({ description = 'Original' }) store.save() local lines = { - '## Inbox', - '/1/- [ ] Original', - '/1/- [ ] Copy of original', + 'Inbox', + '/1/ Original', + '/1/ Copy of original', } diff.apply(lines) store.unload() @@ -126,8 +126,8 @@ describe('diff', function() store.add({ description = 'Moving task', category = 'Inbox' }) store.save() local lines = { - '## Work', - '/1/- [ ] Moving task', + 'Work', + '/1/ Moving task', } diff.apply(lines) store.unload() @@ -140,8 +140,8 @@ describe('diff', function() store.add({ description = 'Stable task', category = 'Inbox' }) store.save() local lines = { - '## Inbox', - '/1/- [ ] Stable task', + 'Inbox', + '/1/ Stable task', } diff.apply(lines) store.unload() @@ -158,8 +158,8 @@ describe('diff', function() store.add({ description = 'Pay bill', due = '2026-03-15' }) store.save() local lines = { - '## Inbox', - '/1/- [ ] Pay bill', + 'Inbox', + '/1/ Pay bill', } diff.apply(lines) store.unload() @@ -172,8 +172,8 @@ describe('diff', function() store.add({ description = 'Task name', priority = 1 }) store.save() local lines = { - '## Inbox', - '/1/- [ ] Task name', + 'Inbox', + '/1/ Task name', } diff.apply(lines) store.unload() diff --git a/spec/store_spec.lua b/spec/store_spec.lua index a2e72aa..930fbc0 100644 --- a/spec/store_spec.lua +++ b/spec/store_spec.lua @@ -92,7 +92,7 @@ describe('store', function() assert.are.equal(1, t1.id) assert.are.equal(2, t2.id) assert.are.equal('pending', t1.status) - assert.are.equal('Todo', t1.category) + assert.are.equal('Inbox', t1.category) end) it('uses provided category', function() diff --git a/spec/views_spec.lua b/spec/views_spec.lua index 4d91e06..9ba12f9 100644 --- a/spec/views_spec.lua +++ b/spec/views_spec.lua @@ -27,7 +27,7 @@ describe('views', function() store.add({ description = 'Task A', category = 'Work' }) store.add({ description = 'Task B', category = 'Work' }) local lines, meta = views.category_view(store.active_tasks()) - assert.are.equal('## Work', lines[1]) + assert.are.equal('Work', lines[1]) assert.are.equal('header', meta[1].type) assert.is_true(lines[2]:find('Task A') ~= nil) assert.is_true(lines[3]:find('Task B') ~= nil) @@ -113,10 +113,10 @@ describe('views', function() task_line = lines[i] end end - assert.are.equal('/1/- [ ] My task', task_line) + assert.are.equal('/1/ My task', task_line) end) - it('formats priority task lines as /ID/- [!] description', function() + it('formats priority task lines as /ID/ ! description', function() store.add({ description = 'Important', category = 'Inbox', priority = 1 }) local lines, meta = views.category_view(store.active_tasks()) local task_line @@ -125,7 +125,7 @@ describe('views', function() task_line = lines[i] end end - assert.are.equal('/1/- [!] Important', task_line) + assert.are.equal('/1/ ! Important', task_line) end) it('sets LineMeta type=header for header lines with correct category', function() @@ -220,8 +220,8 @@ describe('views', function() end end end - assert.are.equal('## Work', first_header) - assert.are.equal('## Inbox', second_header) + assert.are.equal('Work', first_header) + assert.are.equal('Inbox', second_header) end) it('appends categories not in category_order after ordered ones', function() @@ -236,8 +236,8 @@ describe('views', function() table.insert(headers, lines[i]) end end - assert.are.equal('## Work', headers[1]) - assert.are.equal('## Errands', headers[2]) + assert.are.equal('Work', headers[1]) + assert.are.equal('Errands', headers[2]) end) it('preserves insertion order when category_order is empty', function() @@ -250,8 +250,8 @@ describe('views', function() table.insert(headers, lines[i]) end end - assert.are.equal('## Alpha', headers[1]) - assert.are.equal('## Beta', headers[2]) + assert.are.equal('Alpha', headers[1]) + assert.are.equal('Beta', headers[2]) end) end) @@ -325,10 +325,10 @@ describe('views', function() assert.is_true(earlier_row < later_row) end) - it('formats task lines as /ID/- [ ] description', function() + it('formats task lines as /ID/ description', function() store.add({ description = 'My task', category = 'Inbox' }) local lines, _ = views.priority_view(store.active_tasks()) - assert.are.equal('/1/- [ ] My task', lines[1]) + assert.are.equal('/1/ My task', lines[1]) end) it('sets show_category=true for all task meta entries', function() diff --git a/syntax/pending.vim b/syntax/pending.vim index a8a0258..b5f3da1 100644 --- a/syntax/pending.vim +++ b/syntax/pending.vim @@ -3,12 +3,12 @@ if exists('b:current_syntax') endif syntax match taskId /^\/\d\+\// conceal -syntax match taskHeader /^## .*$/ contains=taskId -syntax match taskCheckbox /\[!\]/ contained containedin=taskLine -syntax match taskLine /^\/\d\+\/- \[.\] .*$/ contains=taskId,taskCheckbox +syntax match taskHeader /^\S.*$/ contains=taskId +syntax match taskPriority /!\ze / contained +syntax match taskLine /^\/\d\+\/ .*$/ contains=taskId,taskPriority highlight default link taskHeader PendingHeader -highlight default link taskCheckbox PendingPriority +highlight default link taskPriority PendingPriority highlight default link taskLine Normal let b:current_syntax = 'task'